Publish AOSP Templates Host v1.0

Fix: 194232491
Test: ./gradlew :app:installDebug and showcase
Change-Id: I9b06dc77ec1ff10ac507013463458299fcc0e698
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4d7af7d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+Host/build/**
+Host/.gradle/**
+Host/.idea/**
+Host/app/build/**
+Host/app/apphost/build/**
+Host/app/renderer/build/**
+
diff --git a/Host/app/apphost/build.gradle b/Host/app/apphost/build.gradle
new file mode 100644
index 0000000..26bb402
--- /dev/null
+++ b/Host/app/apphost/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdk 31
+
+    defaultConfig {
+        minSdk 29
+        targetSdk 31
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation "androidx.car.app:app:1.2.0-beta02"
+    compileOnly 'com.google.auto.value:auto-value-annotations:1.9'
+    annotationProcessor 'com.google.auto.value:auto-value:1.9'
+    implementation group: 'com.google.errorprone', name: 'error_prone_annotations', version: '2.11.0'
+    implementation group: 'org.checkerframework', name: 'checker-qual', version: '3.21.1'
+    implementation('com.google.guava:guava:31.0.1-jre')
+    implementation 'com.github.bumptech.glide:glide:4.12.0'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
+
+    implementation 'androidx.appcompat:appcompat:1.4.1'
+    implementation 'com.google.android.material:material:1.5.0'
+    testImplementation 'junit:junit:4.13.2'
+    testImplementation 'org.mockito:mockito-core:4.3.1'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/Host/app/apphost/consumer-rules.pro b/Host/app/apphost/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Host/app/apphost/consumer-rules.pro
diff --git a/Host/app/apphost/proguard-rules.pro b/Host/app/apphost/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Host/app/apphost/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/Host/app/apphost/src/main/AndroidManifest.xml b/Host/app/apphost/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..97e8f2b
--- /dev/null
+++ b/Host/app/apphost/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="com.android.car.libraries.apphost"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <!-- Declares permissions meant for the Car App Library (3p apps) -->
+  <!-- Permission that apps can use to get access to a canvas surface. -->
+  <permission
+      android:name="androidx.car.app.ACCESS_SURFACE"
+      android:protectionLevel="normal"/>
+  <!-- Permission that apps can use to get access to the navigation templates. -->
+  <permission
+      android:name="androidx.car.app.NAVIGATION_TEMPLATES"
+      android:protectionLevel="normal"/>
+  <!-- Permission that apps can use to get access to templates that show a map. -->
+  <permission
+      android:name="androidx.car.app.MAP_TEMPLATES"
+      android:protectionLevel="normal"/>
+</manifest>
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java
new file mode 100644
index 0000000..752227f
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/AbstractHost.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_DISCONNECTED;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_UNBOUND;
+
+import android.content.Intent;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.google.common.base.Preconditions;
+import java.io.PrintWriter;
+
+/**
+ * Abstract base class for {@link Host}s which implements some of the common host service
+ * functionality.
+ */
+public abstract class AbstractHost implements Host {
+  protected TemplateContext mTemplateContext;
+  private boolean mIsValid = true;
+  private final String mName;
+
+  @SuppressWarnings("nullness")
+  protected AbstractHost(TemplateContext templateContext, String name) {
+    mTemplateContext = templateContext;
+    mName = name;
+    addEventSubscriptions();
+  }
+
+  @Override
+  public void setTemplateContext(TemplateContext templateContext) {
+    removeEventSubscriptions();
+    mTemplateContext = templateContext;
+    addEventSubscriptions();
+  }
+
+  @Override
+  public void invalidateHost() {
+    mIsValid = false;
+  }
+
+  @Override
+  public void onCarAppBound() {}
+
+  @Override
+  public void onNewIntentDispatched() {}
+
+  @Override
+  public void onBindToApp(Intent intent) {}
+
+  @Override
+  public void reportStatus(PrintWriter pw, Pii piiHandling) {}
+
+  /** Called when the app is disconnected. */
+  public void onDisconnectedEvent() {}
+
+  /** Called when the app is unbound. */
+  public void onUnboundEvent() {}
+
+  /** Asserts that the service is valid. */
+  protected void assertIsValid() {
+    Preconditions.checkState(mIsValid, "Accessed a host service after it became invalidated");
+  }
+
+  /** Runs the {@code runnable} iff the host is valid. */
+  protected void runIfValid(String methodName, Runnable runnable) {
+    if (isValid()) {
+      runnable.run();
+    } else {
+      L.w(mName, "Accessed %s after host became invalidated", methodName);
+    }
+  }
+
+  /** Returns whether the host is valid. */
+  protected boolean isValid() {
+    return mIsValid;
+  }
+
+  private void addEventSubscriptions() {
+    mTemplateContext
+        .getEventManager()
+        .subscribeEvent(this, APP_DISCONNECTED, this::onDisconnectedEvent);
+    mTemplateContext.getEventManager().subscribeEvent(this, APP_UNBOUND, this::onUnboundEvent);
+  }
+
+  private void removeEventSubscriptions() {
+    mTemplateContext.getEventManager().unsubscribeEvent(this, APP_DISCONNECTED);
+    mTemplateContext.getEventManager().unsubscribeEvent(this, APP_UNBOUND);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java
new file mode 100644
index 0000000..aa557b0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/CarHost.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarContext;
+import androidx.car.app.ICarApp;
+import androidx.car.app.ICarHost;
+import androidx.car.app.versioning.CarAppApiLevels;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.IntentUtils;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.StringUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.internal.CarAppBinding;
+import com.android.car.libraries.apphost.internal.CarAppBindingCallback;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.StatusReporter;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import com.google.common.base.Preconditions;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Host responsible for binding to and maintaining the lifecycle of a single app. */
+public class CarHost implements LifecycleOwner, StatusReporter {
+  // Suppress under-initialization checker warning for passing this to the LifecycleRegistry's
+  // ctor.
+  @SuppressWarnings("nullness")
+  private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
+
+  private final ICarHost.Stub mCarHostStub = new CarHostStubImpl();
+  private final CarAppBinding mCarAppBinding;
+  private final TelemetryHandler mTelemetryHandler;
+
+  // Key is a @CarAppService.
+  private final HashMap<String, Host> mHostServices = new HashMap<>();
+
+  private TemplateContext mTemplateContext;
+  private long mLastStartTimeMillis = -1;
+  private boolean mIsValid = true;
+  private boolean mIsAppBound = false;
+
+  /** Creates a {@link CarHost}. */
+  public static CarHost create(TemplateContext templateContext) {
+    return new CarHost(templateContext);
+  }
+
+  /**
+   * Binds to the app managed by this {@link CarHost} instance.
+   *
+   * @param intent the intent used to start the app.
+   */
+  public void bindToApp(Intent intent) {
+    assertIsValid();
+
+    for (Host host : mHostServices.values()) {
+      host.onBindToApp(intent);
+    }
+
+    // Remove the custom extras we put in the intent, if any.
+    IntentUtils.removeInternalIntentExtras(
+        intent, mTemplateContext.getCarHostConfig().getHostIntentExtrasToRemove());
+
+    mCarAppBinding.bind(intent);
+    mTelemetryHandler.logCarAppTelemetry(
+        TelemetryEvent.newBuilder(UiAction.APP_START, mCarAppBinding.getAppName()));
+  }
+
+  /** Unbinds from the app previously bound to with {@link #bindToApp}. */
+  public void unbindFromApp() {
+    mCarAppBinding.unbind();
+  }
+
+  /**
+   * Registers a {@link Host} with this host and returns the {@link CarHost}. This call is
+   * idempotent for the same {@code type}.
+   *
+   * @param type one of the CarServiceType as defined in {@link CarContext}
+   * @param factory factory for creating the {@link Host} corresponding to the service type
+   */
+  public Host registerHostService(String type, HostFactory factory) {
+    assertIsValid();
+    Host host = mHostServices.get(type);
+    if (host == null) {
+      host = factory.createHost(mCarAppBinding);
+      mHostServices.put(type, host);
+    }
+    return host;
+  }
+
+  /** Updates the {@link TemplateContext} when the template has destroyed an recreated. */
+  public void setTemplateContext(TemplateContext templateContext) {
+    // Since we are updating the TemplateContext, unsubscribe the event listener from the
+    // previous one.
+    mTemplateContext.getEventManager().unsubscribeEvent(this, EventType.CONFIGURATION_CHANGED);
+
+    mTemplateContext = templateContext;
+
+    mTemplateContext.getAppBindingStateProvider().updateAppBindingState(mIsAppBound);
+    mTemplateContext
+        .getEventManager()
+        .subscribeEvent(this, EventType.CONFIGURATION_CHANGED, this::onConfigurationChanged);
+    mCarAppBinding.setTemplateContext(templateContext);
+
+    for (Host host : mHostServices.values()) {
+      host.setTemplateContext(templateContext);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return mCarAppBinding.toString();
+  }
+
+  /**
+   * Returns the {@link Host} that is registered for the given {@code type}.
+   *
+   * @param type one of the CarServiceType as defined in {@link CarContext}
+   * @throws IllegalStateException if there are no services registered for the given {@code type}
+   */
+  public Host getHostOrThrow(String type) {
+    assertIsValid();
+    Host host = mHostServices.get(type);
+    if (host == null) {
+      throw new IllegalStateException("No host service registered for: " + type);
+    }
+    return host;
+  }
+
+  /** Dispatches the given lifecycle event to the app managed by this {host}. */
+  public void dispatchAppLifecycleEvent(Event event) {
+    Log.d(LogTags.APP_HOST, "AppLifecycleEvent: " + event);
+    assertIsValid();
+    mLifecycleRegistry.handleLifecycleEvent(event);
+  }
+
+  /** Invalidates the {@link CarHost} so that any subsequent call on any of the APIs will fail. */
+  public void invalidate() {
+    mIsValid = false;
+    for (Host host : mHostServices.values()) {
+      host.invalidateHost();
+    }
+
+    mLifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY);
+  }
+
+  /** Returns the {@link CarAppBinding} instance used to bind to the app. */
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public CarAppBinding getCarAppBinding() {
+    return mCarAppBinding;
+  }
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public TemplateContext getTemplateContext() {
+    return mTemplateContext;
+  }
+
+  /** Runs the logic necessary after the {@link CarHost} has successfully bound to the app. */
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public void onAppBound() {
+    // Don't assert whether the object is valid here as this is an asynchronous API and could be
+    // called after being invalidated we don't want to cause a crash after the previous
+    // shutdown.
+    if (!mIsValid) {
+      return;
+    }
+
+    mIsAppBound = true;
+    mTemplateContext.getAppBindingStateProvider().updateAppBindingState(mIsAppBound);
+    for (Host host : mHostServices.values()) {
+      host.onCarAppBound();
+    }
+
+    // Binding is asynchronous, so when it completes, the lifecycle events may not have
+    // propagated. When lifecycle events happen before binding is complete, the lifecycle
+    // methods are dropped on the floor. Due to this, we will send lifecycle methods that
+    // may have happened since the bind began.
+    State currentState = mLifecycleRegistry.getCurrentState();
+    if (currentState.isAtLeast(State.STARTED)) {
+      mCarAppBinding.dispatchAppLifecycleEvent(Event.ON_START);
+      if (currentState.isAtLeast(State.RESUMED)) {
+        mCarAppBinding.dispatchAppLifecycleEvent(Event.ON_RESUME);
+      }
+    }
+  }
+
+  @Override
+  public void reportStatus(PrintWriter pw, Pii piiHandling) {
+    pw.printf("- state: %s\n", mLifecycleRegistry.getCurrentState());
+    pw.printf("- is valid: %b\n", mIsValid);
+
+    if (mLastStartTimeMillis >= 0) {
+      long durationMillis =
+          mTemplateContext.getSystemClockWrapper().elapsedRealtime() - mLastStartTimeMillis;
+      pw.printf("- duration: %s\n", StringUtils.formatDuration(durationMillis));
+    }
+
+    mCarAppBinding.reportStatus(pw, piiHandling);
+    mTemplateContext.reportStatus(pw);
+
+    for (Map.Entry<String, Host> entry : mHostServices.entrySet()) {
+      pw.printf("\nHost service: %s\n", entry.getKey());
+      entry.getValue().reportStatus(pw, piiHandling);
+    }
+  }
+
+  @Override
+  public Lifecycle getLifecycle() {
+    // Don't assert whether the object is valid here, since callers may use the lifecycle to
+    // know.
+    return mLifecycleRegistry;
+  }
+
+  /**
+   * Returns the stub for the {@link ICarHost} binder that apps use to communicate with this host.
+   */
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  public ICarHost.Stub getCarHostStub() {
+    return mCarHostStub;
+  }
+
+  void onNewIntentDispatched() {
+    // Don't assert whether the object is valid here as this is an asynchronous API and could be
+    // called after being invalidated we don't want to cause a crash after the previous
+    // shutdown.
+    if (!mIsValid) {
+      return;
+    }
+
+    for (Host host : mHostServices.values()) {
+      host.onNewIntentDispatched();
+    }
+  }
+
+  private void onConfigurationChanged() {
+    if (mCarAppBinding.isBound()) {
+      mCarAppBinding.dispatch(
+          CarContext.CAR_SERVICE,
+          NamedAppServiceCall.create(
+              CarAppApi.ON_CONFIGURATION_CHANGED,
+              (ICarApp carApp, ANRToken anrToken) ->
+                  carApp.onConfigurationChanged(
+                      mTemplateContext.getResources().getConfiguration(),
+                      new OnDoneCallbackStub(mTemplateContext, anrToken))));
+    }
+  }
+
+  @SuppressWarnings("nullness")
+  private CarHost(TemplateContext templateContext) {
+    mTemplateContext = templateContext;
+    mCarAppBinding =
+        CarAppBinding.create(
+            templateContext,
+            mCarHostStub,
+            new CarAppBindingCallback() {
+              @Override
+              public void onCarAppBound() {
+                onAppBound();
+              }
+
+              @Override
+              public void onNewIntentDispatched() {
+                CarHost.this.onNewIntentDispatched();
+              }
+
+              @Override
+              public void onCarAppUnbound() {
+                mIsAppBound = false;
+                templateContext.getAppBindingStateProvider().updateAppBindingState(mIsAppBound);
+                templateContext.getEventManager().dispatchEvent(EventType.APP_UNBOUND);
+              }
+            });
+
+    templateContext
+        .getEventManager()
+        .subscribeEvent(this, EventType.CONFIGURATION_CHANGED, this::onConfigurationChanged);
+
+    mTelemetryHandler = templateContext.getTelemetryHandler();
+
+    mLifecycleRegistry.handleLifecycleEvent(Event.ON_CREATE);
+
+    mLifecycleRegistry.addObserver(
+        new DefaultLifecycleObserver() {
+          @Override
+          public void onStart(LifecycleOwner lifecycleOwner) {
+            mLastStartTimeMillis = templateContext.getSystemClockWrapper().elapsedRealtime();
+            dispatch(Event.ON_START);
+          }
+
+          @Override
+          public void onResume(LifecycleOwner lifecycleOwner) {
+            dispatch(Event.ON_RESUME);
+          }
+
+          @Override
+          public void onPause(LifecycleOwner lifecycleOwner) {
+            dispatch(Event.ON_PAUSE);
+          }
+
+          @Override
+          public void onStop(LifecycleOwner lifecycleOwner) {
+            dispatch(Event.ON_STOP);
+            long durationMillis =
+                templateContext.getSystemClockWrapper().elapsedRealtime() - mLastStartTimeMillis;
+            if (mLastStartTimeMillis < 0 || durationMillis < 0) {
+              L.w(
+                  LogTags.APP_HOST,
+                  "Negative duration %d or negative last start time %d",
+                  durationMillis,
+                  mLastStartTimeMillis);
+              return;
+            }
+            mTelemetryHandler.logCarAppTelemetry(
+                TelemetryEvent.newBuilder(UiAction.APP_RUNTIME, mCarAppBinding.getAppName())
+                    .setDurationMs(durationMillis));
+            mLastStartTimeMillis = -1;
+          }
+
+          private void dispatch(Event event) {
+            if (mCarAppBinding.isBound()) {
+              mCarAppBinding.dispatchAppLifecycleEvent(event);
+            }
+          }
+        });
+  }
+
+  private void assertIsValid() {
+    Preconditions.checkState(mIsValid, "Accessed the car host after it became invalidated");
+  }
+
+  private final class CarHostStubImpl extends ICarHost.Stub {
+    @Override
+    public void startCarApp(Intent intent) {
+      mTemplateContext.getCarAppManager().startCarApp(intent);
+    }
+
+    @Override
+    public void finish() {
+      mTemplateContext.getCarAppManager().finishCarApp();
+    }
+
+    @Override
+    public IBinder getHost(String type) {
+      assertIsValid();
+      Host service = mHostServices.get(type);
+      if (CarContext.NAVIGATION_SERVICE.equals(type)
+          && !mTemplateContext.getCarAppPackageInfo().isNavigationApp()) {
+        throw new IllegalArgumentException(
+            "Attempted to retrieve the navigation service, but the app is not a"
+                + " navigation app");
+      } else if (CarContext.CONSTRAINT_SERVICE.equals(type)
+          && mTemplateContext.getCarHostConfig().getNegotiatedApi() < CarAppApiLevels.LEVEL_2) {
+        throw new IllegalArgumentException(
+            "Attempted to retrieve the constraint service, but the host's API level is"
+                + " less than "
+                + CarAppApiLevels.LEVEL_2);
+      } else if (CarContext.HARDWARE_SERVICE.equals(type)
+          && mTemplateContext.getCarHostConfig().getNegotiatedApi() < CarAppApiLevels.LEVEL_3) {
+        throw new IllegalArgumentException(
+            "Attempted to retrieve the hardware service, but the host's API level is"
+                + " less than "
+                + CarAppApiLevels.LEVEL_3);
+      }
+
+      if (service != null) {
+        return service.getBinder();
+      }
+
+      throw new IllegalArgumentException("Unknown host service type:" + type);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java
new file mode 100644
index 0000000..b76f074
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/Host.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost;
+
+import android.content.Intent;
+import android.os.IBinder;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.StatusReporter;
+
+/**
+ * A host that manages app specific behaviors such as managing template APIs, navigation APIs, etc.
+ *
+ * <p>This should be registered with {@link CarHost}.
+ */
+public interface Host extends StatusReporter {
+  /** Invalidates the {@link Host} so that any subsequent call on any of the APIs will fail. */
+  void invalidateHost();
+
+  /** Informs the {@link Host} that an {@link Intent} has been received to bind to the app. */
+  void onBindToApp(Intent intent);
+
+  /** Indicates that the {@link CarHost} is now bound to the app. */
+  void onCarAppBound();
+
+  /** Indicates that a {@code onNewIntent} call has been dispatched to the app. */
+  void onNewIntentDispatched();
+
+  /** Returns the binder interface that the app can use to talk to this host. */
+  IBinder getBinder();
+
+  /** Sets the updated {@link TemplateContext} in this host instance. */
+  void setTemplateContext(TemplateContext templateContext);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.java
new file mode 100644
index 0000000..f0cb3bb
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/HostFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost;
+
+import com.android.car.libraries.apphost.internal.CarAppBinding;
+
+/** A factory of {@link Host} instances. */
+public interface HostFactory {
+  /**
+   * Creates a {@link Host} instance.
+   *
+   * @param appBinding the binding to use to dispatch calls to the client. This is upper bounded to
+   *     {@link Object} and down-casted later to avoid making {@link CarAppBinding} public, while
+   *     allowing round-tripping it outside of the package
+   */
+  Host createHost(Object appBinding);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java
new file mode 100644
index 0000000..1219bad
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/ManagerDispatcher.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost;
+
+import android.content.ComponentName;
+import android.os.IInterface;
+import androidx.annotation.AnyThread;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.internal.CarAppBinding;
+
+/**
+ * A one-way dispatcher of calls to the client app.
+ *
+ * @param <ServiceT> The type of service to dispatch calls for.
+ */
+public abstract class ManagerDispatcher<ServiceT extends IInterface> {
+  private final String mManagerType;
+  private final CarAppBinding mAppBinding;
+
+  public ComponentName getAppName() {
+    return mAppBinding.getAppName();
+  }
+
+  protected ManagerDispatcher(String managerType, Object appBinding) {
+    mManagerType = managerType;
+    mAppBinding = (CarAppBinding) appBinding;
+  }
+
+  /** Dispatches the {@code call} to the appropriate app service. */
+  @AnyThread
+  protected void dispatch(NamedAppServiceCall<ServiceT> call) {
+    mAppBinding.dispatch(mManagerType, call);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java
new file mode 100644
index 0000000..e4d0584
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/NavigationIntentConverter.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost;
+
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import androidx.car.app.CarContext;
+import androidx.car.app.model.CarLocation;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import java.util.List;
+
+/**
+ * A helper class to convert template navigation {@link Intent}s to/from legacy format.
+ *
+ * <p>Legacy apps are navigation apps that are non template (gmm, waze and kakao).
+ *
+ * <p>Legacy apps currently support a "https://maps.google.com/maps" uri, which we are not going to
+ * force all nav apps to support.
+ *
+ * <p>There are other navigation uris that some legacy apps support, such as "google.navigation:" or
+ * "google.maps:", but not all of them do.
+ *
+ * <p>The format for the uri for new navigation apps is described at {@link CarContext#startCarApp}.
+ */
+public final class NavigationIntentConverter {
+  public static final String GEO_QUERY_PREFIX = "geo";
+
+  private static final String LEGACY_NAVIGATION_INTENT_DATA_PREFIX =
+      "https://maps.google.com/maps?nav=1&q=";
+
+  private static final String NAV_PREFIX = "google.navigation";
+  private static final String MAPS_PREFIX = "google.maps";
+
+  private static final String HTTP_MAPS_URL_PREFIX = "http://maps.google.com";
+  private static final String HTTPS_MAPS_URL_PREFIX = "https://maps.google.com";
+  private static final String HTTPS_ASSISTANT_MAPS_URL_PREFIX = "https://assistant-maps.google.com";
+
+  private static final String TEMPLATE_NAVIGATION_INTENT_DATA_LAT_LNG_PREFIX =
+      GEO_QUERY_PREFIX + ":";
+  private static final String TEMPLATE_NAVIGATION_INTENT_DATA_PREFIX =
+      TEMPLATE_NAVIGATION_INTENT_DATA_LAT_LNG_PREFIX + "0,0?q=";
+
+  private static final String SEARCH_QUERY_PARAMETER = "q";
+  private static final String SEARCH_QUERY_PARAMETER_SPLITTER = SEARCH_QUERY_PARAMETER + "=";
+  private static final String ADDRESS_QUERY_PARAMETER = "daddr";
+  private static final String ADDRESS_QUERY_PARAMETER_SPLITTER = ADDRESS_QUERY_PARAMETER + "=";
+
+  /**
+   * Converts the given {@code navIntent} to one that is supported by legacy apps.
+   *
+   * <p>This method <strong>will update</strong> the {@link Intent} provided.
+   *
+   * @see CarContext#startCarApp for format documentation
+   */
+  public static void toLegacyNavIntent(Intent navIntent) {
+    L.d(LogTags.APP_HOST, "Converting to legacy nav intent %s", navIntent);
+
+    navIntent.setAction(Intent.ACTION_VIEW);
+
+    Uri navUri = Preconditions.checkNotNull(navIntent.getData());
+
+    // Cleanup by removing spaces.
+    CarLocation location = getCarLocation(navUri);
+
+    if (location != null) {
+      navIntent.setData(
+          Uri.parse(
+              LEGACY_NAVIGATION_INTENT_DATA_PREFIX
+                  + location.getLatitude()
+                  + ","
+                  + location.getLongitude()));
+    } else {
+      String query = getQueryString(navUri);
+      if (query == null) {
+        throw new IllegalArgumentException("Navigation intent is not properly formed");
+      }
+      navIntent.setData(
+          Uri.parse(LEGACY_NAVIGATION_INTENT_DATA_PREFIX + query.replaceAll("\\s", "+")));
+    }
+    L.d(LogTags.APP_HOST, "Converted to legacy nav intent %s", navIntent);
+  }
+
+  /** Verifies if the given {@link Intent} is for navigation with a legacy navigation app. */
+  public static boolean isLegacyNavIntent(Intent intent) {
+    Uri uri = intent.getData();
+
+    if (uri == null) {
+      return false;
+    }
+
+    String scheme = uri.getScheme();
+    String dataString = intent.getDataString();
+    return GEO_QUERY_PREFIX.equals(scheme)
+        || NAV_PREFIX.equals(scheme)
+        || MAPS_PREFIX.equals(scheme)
+        || Strings.nullToEmpty(dataString).startsWith(HTTP_MAPS_URL_PREFIX) // NOLINT
+        || Strings.nullToEmpty(dataString).startsWith(HTTPS_MAPS_URL_PREFIX) // NOLINT
+        || Strings.nullToEmpty(dataString).startsWith(HTTPS_ASSISTANT_MAPS_URL_PREFIX); // NOLINT
+  }
+
+  /**
+   * Converts the given {@code legacyIntent} to one that is supported by template navigation apps.
+   *
+   * <p>This method <strong>will update</strong> the {@link Intent} provided.
+   *
+   * @see CarContext#startCarApp for the template navigation {@link Intent} format
+   */
+  public static void fromLegacyNavIntent(Intent legacyIntent) {
+    L.d(LogTags.APP_HOST, "Converting from legacy nav intent %s", legacyIntent);
+    Preconditions.checkArgument(isLegacyNavIntent(legacyIntent));
+
+    legacyIntent.setAction(CarContext.ACTION_NAVIGATE);
+
+    Uri uri = Preconditions.checkNotNull(legacyIntent.getData());
+
+    CarLocation location = getCarLocation(uri);
+
+    if (location != null) {
+      legacyIntent.setData(
+          Uri.parse(
+              TEMPLATE_NAVIGATION_INTENT_DATA_LAT_LNG_PREFIX
+                  + location.getLatitude()
+                  + ","
+                  + location.getLongitude()));
+    } else {
+      String query = getQueryString(uri);
+      if (query == null) {
+        throw new IllegalArgumentException("Navigation intent is not properly formed");
+      }
+      legacyIntent.setData(
+          Uri.parse(TEMPLATE_NAVIGATION_INTENT_DATA_PREFIX + query.replaceAll("\\s", "+")));
+    }
+    L.d(LogTags.APP_HOST, "Converted from legacy nav intent %s", legacyIntent);
+  }
+
+  /**
+   * Returns the latitude, longitude from the {@link Uri}, or {@code null} if none exists.
+   *
+   * <p>e.g. if Uri string is "geo:123.45,98.09", return value will be a {@link CarLocation} with
+   * 123.45 latitude and 98.09 longitude.
+   *
+   * <p>e.g. if Uri string is "https://maps.google.com/maps?q=123.45,98.09&nav=1", return value will
+   * be a {@link CarLocation} with 123.45 latitude and 98.09 longitude.
+   */
+  @Nullable
+  public static CarLocation getCarLocation(Uri uri) {
+    String possibleLatLng = getQueryString(uri);
+    if (possibleLatLng == null) {
+      // If not after a q=, uri is valid as geo:12.34,34.56
+      possibleLatLng = uri.getEncodedSchemeSpecificPart();
+    }
+
+    List<String> latLngParts = Splitter.on(',').splitToList(possibleLatLng);
+    if (latLngParts.size() == 2) {
+      try {
+        // Ensure both parts are doubles.
+        return CarLocation.create(
+            Double.parseDouble(latLngParts.get(0)), Double.parseDouble(latLngParts.get(1)));
+      } catch (NumberFormatException e) {
+        // Values are not Doubles.
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns the actual query from the {@link Uri}, or {@code null} if none exists.
+   *
+   * <p>The query will be after "q=" or "daddr=".
+   *
+   * <p>e.g. if Uri string is "geo:0,0?q=124+Foo+St", return value will be "124+Foo+St".
+   *
+   * <p>e.g. if Uri string is "https://maps.google.com/maps?daddr=123+main+st&nav=1", return value
+   * will be "123+main+st".
+   */
+  @Nullable
+  public static String getQueryString(Uri uri) {
+    if (uri.isHierarchical()) {
+      List<String> query = uri.getQueryParameters(SEARCH_QUERY_PARAMETER);
+
+      if (query.isEmpty()) {
+        // No q= parameter, check if there is a daddr= parameter.
+        query = uri.getQueryParameters(ADDRESS_QUERY_PARAMETER);
+      }
+      return Iterables.getFirst(query, null);
+    }
+
+    String schemeSpecificPart = uri.getEncodedSchemeSpecificPart();
+    List<String> parts =
+        Splitter.on(SEARCH_QUERY_PARAMETER_SPLITTER).splitToList(schemeSpecificPart);
+
+    if (parts.size() < 2) {
+      // Did not find "q=".
+      parts = Splitter.on(ADDRESS_QUERY_PARAMETER_SPLITTER).splitToList(schemeSpecificPart);
+    }
+
+    // If we have a valid split on "q=" or "daddr=", split on "&" to only get the one parameter.
+    return parts.size() < 2 ? null : Splitter.on("&").splitToList(parts.get(1)).get(0);
+  }
+
+  private NavigationIntentConverter() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java
new file mode 100644
index 0000000..6ffca5b
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/** Handles checking if an app does not respond in a timely manner. */
+public interface ANRHandler {
+  /** Time to wait for ANR check. */
+  int ANR_TIMEOUT_MS = 5000;
+
+  /**
+   * Performs the call and checks for application not responding.
+   *
+   * <p>The ANR check will happen in {@link #ANR_TIMEOUT_MS} milliseconds after calling {@link
+   * ANRCheckingCall#call}.
+   */
+  void callWithANRCheck(CarAppApi carAppApi, ANRCheckingCall call);
+
+  /** Token for dismissing the ANR check. */
+  interface ANRToken {
+    /** Requests dismissal of the ANR check. */
+    void dismiss();
+
+    /** Returns the {@link CarAppApi} that this token is for. */
+    CarAppApi getCarAppApi();
+  }
+
+  /** A call that checks for ANR and receives a token to use for dismissing the ANR check. */
+  interface ANRCheckingCall {
+    /**
+     * Performs the call.
+     *
+     * @param anrToken the token to use for dismissing the ANR check when the app calls back
+     */
+    void call(ANRToken anrToken);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java
new file mode 100644
index 0000000..4a44b25
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+/** Defines which type of incompatibility this exception is for. */
+public enum ApiIncompatibilityType {
+  APP_TOO_OLD,
+  HOST_TOO_OLD;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java
new file mode 100644
index 0000000..a3a4b8e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+/** Container class for letting the rest of the host knows whether the app is bound. */
+public final class AppBindingStateProvider {
+
+  private boolean mIsAppBound = false;
+
+  /** Returns whether the app is bound. */
+  public boolean isAppBound() {
+    return mIsAppBound;
+  }
+
+  /** Updates the app binding state to the input value. */
+  public void updateAppBindingState(boolean isAppBound) {
+    mIsAppBound = isAppBound;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java
new file mode 100644
index 0000000..25d9ee3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import android.graphics.Rect;
+import android.os.RemoteException;
+import androidx.car.app.ISurfaceCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.SurfaceContainer;
+import androidx.car.app.model.InputCallbackDelegate;
+import androidx.car.app.model.OnCheckedChangeDelegate;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.OnContentRefreshDelegate;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import androidx.car.app.model.SearchCallbackDelegate;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/**
+ * Class to set up safe remote callbacks to apps.
+ *
+ * <p>App interfaces to client are {@code oneway} so the calling thread does not block waiting for a
+ * response. (see go/aidl-best-practices for more information).
+ */
+public interface AppDispatcher {
+  /**
+   * Dispatches a {@link ISurfaceCallback#onSurfaceAvailable} to the provided listener with the
+   * provided container.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchSurfaceAvailable(
+      ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer);
+
+  /**
+   * Dispatches a {@link ISurfaceCallback#onSurfaceDestroyed} to the provided listener with the
+   * provided container.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchSurfaceDestroyed(
+      ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer);
+
+  /**
+   * Dispatches a {@link ISurfaceCallback#onVisibleAreaChanged} to the provided listener with the
+   * provided area.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchVisibleAreaChanged(ISurfaceCallback surfaceListener, Rect visibleArea);
+
+  /**
+   * Dispatches a {@link ISurfaceCallback#onStableAreaChanged} to the provided listener with the
+   * provided area.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchStableAreaChanged(ISurfaceCallback surfaceListener, Rect stableArea);
+
+  /**
+   * Dispatches a {@link ISurfaceCallback#onScroll} to the provided listener with the provided
+   * scroll distance.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchOnSurfaceScroll(ISurfaceCallback surfaceListener, float distanceX, float distanceY);
+
+  /**
+   * Dispatches a {@link ISurfaceCallback#onFling} to the provided listener with the provided fling
+   * velocity.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchOnSurfaceFling(ISurfaceCallback surfaceListener, float velocityX, float velocityY);
+
+  /**
+   * Dispatches a {@link ISurfaceCallback#onScale} to the provided listener with the provided focal
+   * point and scale factor.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchOnSurfaceScale(
+      ISurfaceCallback surfaceListener, float focusX, float focusY, float scaleFactor);
+
+  /**
+   * Dispatches a {@link SearchCallbackDelegate#sendSearchTextChanged} to the provided listener with
+   * the provided search text.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchSearchTextChanged(SearchCallbackDelegate searchCallbackDelegate, String searchText);
+
+  /**
+   * Dispatches a {@link SearchCallbackDelegate#sendSearchSubmitted} to the provided listener with
+   * the provided search text.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchSearchSubmitted(SearchCallbackDelegate searchCallbackDelegate, String searchText);
+
+  /**
+   * Dispatches an {@link InputCallbackDelegate#sendInputTextChanged} to the provided listener with
+   * the provided input text.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchInputTextChanged(InputCallbackDelegate inputCallbackDelegate, String inputText);
+
+  /**
+   * Dispatches an {@link InputCallbackDelegate#sendInputSubmitted} to the provided listener with
+   * the provided input text.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchInputSubmitted(InputCallbackDelegate inputCallbackDelegate, String inputText);
+
+  /**
+   * Dispatches a {@link OnItemVisibilityChangedDelegate#sendItemVisibilityChanged} to the provided
+   * listener.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchItemVisibilityChanged(
+      OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate,
+      int startIndexInclusive,
+      int endIndexExclusive);
+
+  /**
+   * Dispatches a {@link OnSelectedDelegate#sendSelected} to the provided listener.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchSelected(OnSelectedDelegate onSelectedDelegate, int index);
+
+  /**
+   * Dispatches a {@link OnCheckedChangeDelegate#sendCheckedChange} to the provided listener.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchCheckedChanged(OnCheckedChangeDelegate onCheckedChangeDelegate, boolean isChecked);
+
+  /**
+   * Dispatches a {@link PanModeDelegate#sendPanModeChanged(boolean, OnDoneCallback)} to the
+   * provided listener.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchPanModeChanged(PanModeDelegate panModeDelegate, boolean isChecked);
+
+  /**
+   * Dispatches a {@link OnClickDelegate#sendClick} to the provided listener.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchClick(OnClickDelegate onClickDelegate);
+
+  /**
+   * Dispatches a {@link OnContentRefreshDelegate#sendContentRefreshRequested} event.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi) for information on error handling
+   */
+  void dispatchContentRefreshRequest(OnContentRefreshDelegate onContentRefreshDelegate);
+
+  /**
+   * Performs the IPC.
+   *
+   * <p>The calls are oneway. Given this any exception thrown by the client will not reach us, they
+   * will be in their own process. (see go/aidl-best-practices for more information).
+   *
+   * <p>This method will handle app exceptions (described below) as well as {@link BundlerException}
+   * which would be thrown if the host fails to bundle an object before sending it over (should
+   * never happen).
+   *
+   * <h1>App Exceptions</h1>
+   *
+   * <p>Here are the possible exceptions thrown by the app, and when they may happen.
+   *
+   * <dl>
+   *   <dt>{@link RemoteException}
+   *   <dd>This exception is thrown when the binder is dead (i.e. the app crashed).
+   *   <dt>{@link RuntimeException}
+   *   <dd>The should not happen in regular scenario. The only cases where may happen are if the app
+   *       is running in the same process as the host, or if the IPC was wrongly configured to not
+   *       be {@code oneway}.
+   * </dl>
+   *
+   * <p>The following are the types of {@link RuntimeException} that the binder let's through. See
+   * https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2061-2094
+   *
+   * <ul>
+   *   <li>{@link SecurityException}
+   *   <li>{@link android.os.BadParcelableException}
+   *   <li>{@link IllegalArgumentException}
+   *   <li>{@link NullPointerException}
+   *   <li>{@link IllegalStateException}
+   *   <li>{@link android.os.NetworkOnMainThreadException}
+   *   <li>{@link UnsupportedOperationException}
+   *   <li>{@link android.os.ServiceSpecificException}
+   *   <li>{@link RuntimeException} - for any other exceptions.
+   * </ul>
+   */
+  void dispatch(OneWayIPC ipc, CarAppApi carAppApi);
+
+  /**
+   * Performs the IPC allowing caller to define behavior for handling any exceptions.
+   *
+   * @see #dispatch(OneWayIPC, CarAppApi)
+   */
+  void dispatch(OneWayIPC ipc, ExceptionHandler exceptionHandler, CarAppApi carAppApi);
+
+  /** Will handle exceptions received while performing a {@link OneWayIPC}. */
+  interface ExceptionHandler {
+    void handle(CarAppError carAppError);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java
new file mode 100644
index 0000000..83d69a0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/** Defines a service that can be retrieved from a {@link TemplateContext} */
+public interface AppHostService {}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java
new file mode 100644
index 0000000..94ec4c8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+
+/** Interface that allows loading application icons */
+public interface AppIconLoader {
+
+  /**
+   * Returns a rounded app icon for the given {@link ComponentName}, or a default icon if the given
+   * {@link ComponentName} doesn't match an installed application.
+   *
+   * <p>Implementations must ensure method is thread-safe.
+   */
+  @NonNull
+  Drawable getRoundAppIcon(@NonNull Context context, @NonNull ComponentName componentName);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java
new file mode 100644
index 0000000..4d95823
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.os.RemoteException;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+
+/**
+ * Defines a call to make to an app service.
+ *
+ * @param <ServiceT> the service to receive the call
+ */
+public interface AppServiceCall<ServiceT> {
+  /** Dispatches the call. */
+  void dispatch(ServiceT appService, ANRToken anrToken) throws RemoteException;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java
new file mode 100644
index 0000000..3b6e46e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/** Interface for handling back button press. */
+public interface BackPressedHandler {
+
+  /**
+   * Forwards a back pressed event to the car app's {@link
+   * androidx.car.app.IAppManager#onBackPressed}.
+   */
+  void onBackPressed();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java
new file mode 100644
index 0000000..c297120
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.Context;
+import android.content.res.Resources;
+import androidx.annotation.ColorInt;
+
+/** A container class for a car app's primary and secondary colors. */
+public class CarAppColors {
+  @ColorInt public final int primaryColor;
+  @ColorInt public final int primaryDarkColor;
+  @ColorInt public final int secondaryColor;
+  @ColorInt public final int secondaryDarkColor;
+
+  /** Constructs an instance of {@link CarAppColors}. */
+  public CarAppColors(
+      int primaryColor, int primaryDarkColor, int secondaryColor, int secondaryDarkColor) {
+    this.primaryColor = primaryColor;
+    this.primaryDarkColor = primaryDarkColor;
+    this.secondaryColor = secondaryColor;
+    this.secondaryDarkColor = secondaryDarkColor;
+  }
+
+  /** Returns a default {@link CarAppColors} to use, based on the host's default colors. */
+  public static CarAppColors getDefault(Context context, HostResourceIds hostResourceIds) {
+    Resources resources = context.getResources();
+    return new CarAppColors(
+        resources.getColor(hostResourceIds.getDefaultPrimaryColor()),
+        resources.getColor(hostResourceIds.getDefaultPrimaryDarkColor()),
+        resources.getColor(hostResourceIds.getDefaultSecondaryColor()),
+        resources.getColor(hostResourceIds.getDefaultSecondaryDarkColor()));
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java
new file mode 100644
index 0000000..edb7355
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.ComponentName;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A class that encapsulates an error message that occurs for an app. */
+public class CarAppError {
+  /** Error type. Each type corresponds to an specific message to be displayed to the user */
+  public enum Type {
+    /** The client application is not responding in timely fashion */
+    ANR_TIMEOUT,
+
+    /** The user has requested to wait for the application to respond */
+    ANR_WAITING,
+
+    /** The client is using a version of the SDK that is not compatible with this host */
+    INCOMPATIBLE_CLIENT_VERSION,
+
+    /** The client does not have a required permission */
+    MISSING_PERMISSION,
+  }
+
+  private final ComponentName mAppName;
+  @Nullable private final Type mType;
+  @Nullable private final Throwable mCause;
+  @Nullable private final String mDebugMessage;
+  @Nullable private final Runnable mExtraAction;
+  private final boolean mLogVerbose;
+
+  /** Returns a {@link Builder} for the given {@code appName}. */
+  public static Builder builder(ComponentName appName) {
+    return new Builder(appName);
+  }
+
+  /** Returns the {@link ComponentName} representing an app. */
+  public ComponentName getAppName() {
+    return mAppName;
+  }
+
+  /**
+   * Returns the error type or {@code null} to show a generic error message.
+   *
+   * @see Builder#setType
+   */
+  @Nullable
+  public Type getType() {
+    return mType;
+  }
+
+  /**
+   * Returns the debug message for displaying in the DHU or any head unit on debug builds.
+   *
+   * @see Builder#setDebugMessage
+   */
+  @Nullable
+  public String getDebugMessage() {
+    return mDebugMessage;
+  }
+
+  /**
+   * Returns the debug message for displaying in the DHU or any head unit on debug builds.
+   *
+   * @see Builder#setCause
+   */
+  @Nullable
+  public Throwable getCause() {
+    return mCause;
+  }
+
+  /**
+   * Returns the {@code action} for the error screen shown to the user, on top of the exit which is
+   * default.
+   *
+   * @see Builder#setExtraAction
+   */
+  @Nullable
+  public Runnable getExtraAction() {
+    return mExtraAction;
+  }
+
+  /**
+   * Returns whether to log this {@link CarAppError} as a verbose log.
+   *
+   * <p>The default is to log as error, but can be overridden via {@link Builder#setLogVerbose}
+   */
+  public boolean logVerbose() {
+    return mLogVerbose;
+  }
+
+  @Override
+  public String toString() {
+    return "[app: "
+        + mAppName
+        + ", type: "
+        + mType
+        + ", cause: "
+        + (mCause != null
+            ? mCause.getClass().getCanonicalName() + ": " + mCause.getMessage()
+            : null)
+        + ", debug msg: "
+        + mDebugMessage
+        + "]";
+  }
+
+  private CarAppError(Builder builder) {
+    mAppName = builder.mAppName;
+    mType = builder.mType;
+    mCause = builder.mCause;
+    mDebugMessage = builder.mDebugMessage;
+    mExtraAction = builder.mExtraAction;
+    mLogVerbose = builder.mLogVerbose;
+  }
+
+  /** A builder for {@link CarAppError}. */
+  public static class Builder {
+    private final ComponentName mAppName;
+    @Nullable private Type mType;
+    @Nullable private Throwable mCause;
+    @Nullable private String mDebugMessage;
+    @Nullable private Runnable mExtraAction;
+    public boolean mLogVerbose;
+
+    private Builder(ComponentName appName) {
+      mAppName = appName;
+    }
+
+    /** Sets the error type, or {@code null} to show a generic error message. */
+    public Builder setType(Type type) {
+      mType = type;
+      return this;
+    }
+
+    /** Sets the exception for displaying in the DHU or any head unit on debug builds. */
+    public Builder setCause(Throwable cause) {
+      mCause = cause;
+      return this;
+    }
+
+    /** Sets the debug message for displaying in the DHU or any head unit on debug builds. */
+    public Builder setDebugMessage(String debugMessage) {
+      mDebugMessage = debugMessage;
+      return this;
+    }
+
+    /**
+     * Adds the {@code action} to the error screen shown to the user, on top of the exit which is
+     * default.
+     */
+    public Builder setExtraAction(Runnable extraAction) {
+      mExtraAction = extraAction;
+      return this;
+    }
+
+    /** Sets whether to log the {@link CarAppError} as verbose only. */
+    public Builder setLogVerbose(boolean logVerbose) {
+      mLogVerbose = logVerbose;
+      return this;
+    }
+
+    /** Constructs the {@link CarAppError} instance. */
+    public CarAppError build() {
+      return new CarAppError(this);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java
new file mode 100644
index 0000000..f06342e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.Intent;
+
+/** Controls the ability to start a new car app, as well as finish the current car app. */
+public interface CarAppManager {
+  /**
+   * Starts a car app on the car screen.
+   *
+   * @see androidx.car.app.CarContext#startCarApp
+   */
+  void startCarApp(Intent intent);
+
+  /** Unbinds from the car app, and goes to the app launcher if the app is currently foreground. */
+  void finishCarApp();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java
new file mode 100644
index 0000000..24b0772
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.ComponentName;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+
+/** Provides package information of a car app. */
+public interface CarAppPackageInfo {
+  /** Package and service name of the 3p car app. */
+  @NonNull
+  ComponentName getComponentName();
+
+  /**
+   * Returns the primary and secondary colors of the app as defined in the metadata entry for the
+   * app service, or default app theme if the metadata entry is not specified.
+   */
+  @NonNull
+  CarAppColors getAppColors();
+
+  /** Returns whether this app info is for a navigation app. */
+  boolean isNavigationApp();
+
+  /** Returns a round app icon for the given car app. */
+  @NonNull
+  Drawable getRoundAppIcon();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java
new file mode 100644
index 0000000..bc272f8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import static androidx.car.app.model.CarColor.TYPE_BLUE;
+import static androidx.car.app.model.CarColor.TYPE_CUSTOM;
+import static androidx.car.app.model.CarColor.TYPE_DEFAULT;
+import static androidx.car.app.model.CarColor.TYPE_GREEN;
+import static androidx.car.app.model.CarColor.TYPE_PRIMARY;
+import static androidx.car.app.model.CarColor.TYPE_RED;
+import static androidx.car.app.model.CarColor.TYPE_SECONDARY;
+import static androidx.car.app.model.CarColor.TYPE_YELLOW;
+import static androidx.core.graphics.ColorUtils.calculateContrast;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_PRIMARY;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_PRIMARY_DARK;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_SECONDARY;
+import static com.android.car.libraries.apphost.common.ColorUtils.KEY_COLOR_SECONDARY_DARK;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.util.Pair;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import com.android.car.libraries.apphost.distraction.constraints.CarColorConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/** Utilities for handling {@link CarColor} instances. */
+public class CarColorUtils {
+
+  private static final double MINIMUM_COLOR_CONTRAST = 4.5;
+
+  /**
+   * Resolves a standard color to a {@link ColorInt}.
+   *
+   * @return the resolved color or {@code defaultColor} if the input {@code carColor} is {@code
+   *     null}, does not meet the constraints, or of the type {@link CarColor#DEFAULT}
+   */
+  @ColorInt
+  public static int resolveColor(
+      TemplateContext templateContext,
+      @Nullable CarColor carColor,
+      boolean isDark,
+      @ColorInt int defaultColor,
+      CarColorConstraints constraints) {
+    return resolveColor(
+        templateContext, carColor, isDark, defaultColor, constraints, Color.TRANSPARENT);
+  }
+
+  /**
+   * Resolves a standard color to a {@link ColorInt}.
+   *
+   * <p>If {@code backgroundColor} is set to {@link Color#TRANSPARENT}, the {@code carColor} will
+   * not be checked for the minimum color contrast.
+   *
+   * @return the resolved color or {@code defaultColor} if the input {@code carColor} is {@code
+   *     null}, does not meet the constraints or minimum color contrast, or of the type {@link
+   *     CarColor#DEFAULT}
+   */
+  @ColorInt
+  public static int resolveColor(
+      TemplateContext templateContext,
+      @Nullable CarColor carColor,
+      boolean isDark,
+      @ColorInt int defaultColor,
+      CarColorConstraints constraints,
+      @ColorInt int backgroundColor) {
+    if (carColor == null) {
+      return defaultColor;
+    }
+    try {
+      constraints.validateOrThrow(carColor);
+    } catch (IllegalArgumentException e) {
+      L.e(LogTags.TEMPLATE, e, "Validation failed for color %s, will use default", carColor);
+      return defaultColor;
+    }
+
+    CarAppPackageInfo info = templateContext.getCarAppPackageInfo();
+    CarAppColors carAppColors = info.getAppColors();
+    HostResourceIds hostResourceIds = templateContext.getHostResourceIds();
+    return resolveColor(
+        templateContext,
+        isDark,
+        carColor,
+        carAppColors,
+        hostResourceIds,
+        defaultColor,
+        backgroundColor);
+  }
+
+  /** Resolves a standard color to a {@link ColorInt}. */
+  @ColorInt
+  public static int resolveColor(
+      Context context,
+      boolean isDark,
+      @Nullable CarColor carColor,
+      CarAppColors carAppColors,
+      HostResourceIds resIds,
+      @ColorInt int defaultColor,
+      @ColorInt int backgroundColor) {
+    if (carColor == null) {
+      return defaultColor;
+    }
+    int type = carColor.getType();
+    Resources resources = context.getResources();
+    switch (type) {
+      case TYPE_DEFAULT:
+        return defaultColor;
+      case TYPE_PRIMARY:
+        return getContrastCheckedColor(
+            carAppColors.primaryColor,
+            carAppColors.primaryDarkColor,
+            backgroundColor,
+            defaultColor,
+            isDark);
+      case TYPE_SECONDARY:
+        return getContrastCheckedColor(
+            carAppColors.secondaryColor,
+            carAppColors.secondaryDarkColor,
+            backgroundColor,
+            defaultColor,
+            isDark);
+      case TYPE_RED:
+        return resources.getColor(isDark ? resIds.getRedDarkColor() : resIds.getRedColor());
+      case TYPE_GREEN:
+        return resources.getColor(isDark ? resIds.getGreenDarkColor() : resIds.getGreenColor());
+      case TYPE_BLUE:
+        return resources.getColor(isDark ? resIds.getBlueDarkColor() : resIds.getBlueColor());
+      case TYPE_YELLOW:
+        return resources.getColor(isDark ? resIds.getYellowDarkColor() : resIds.getYellowColor());
+      case TYPE_CUSTOM:
+        return getContrastCheckedColor(
+            carColor.getColor(), carColor.getColorDark(), backgroundColor, defaultColor, isDark);
+      default:
+        L.e(LogTags.TEMPLATE, "Failed to resolve standard color id: %d", type);
+        return defaultColor;
+    }
+  }
+
+  /**
+   * Returns the {@link CarAppColors} from the given app name if all primary and secondary colors
+   * are present in the app's manifest, otherwise returns {@link CarAppColors#getDefault(Context,
+   * HostResourceIds)}.
+   */
+  public static CarAppColors resolveAppColor(
+      @NonNull Context context,
+      @NonNull ComponentName appName,
+      @NonNull HostResourceIds hostResourceIds) {
+    String packageName = appName.getPackageName();
+    CarAppColors defaultColors = CarAppColors.getDefault(context, hostResourceIds);
+
+    int themeId = ColorUtils.loadThemeId(context, appName);
+    if (themeId == 0) {
+      L.w(LogTags.TEMPLATE, "Cannot get the app theme from %s", packageName);
+      return defaultColors;
+    }
+
+    Context packageContext = ColorUtils.getPackageContext(context, packageName);
+    if (packageContext == null) {
+      L.w(LogTags.TEMPLATE, "Cannot get the app context from %s", packageName);
+      return defaultColors;
+    }
+    packageContext.setTheme(themeId);
+
+    Resources.Theme theme = packageContext.getTheme();
+    Pair<Integer, Integer> primaryColorVariants =
+        ColorUtils.getColorVariants(
+            theme,
+            packageName,
+            KEY_COLOR_PRIMARY,
+            KEY_COLOR_PRIMARY_DARK,
+            defaultColors.primaryColor,
+            defaultColors.primaryDarkColor);
+    Pair<Integer, Integer> secondaryColorVariants =
+        ColorUtils.getColorVariants(
+            theme,
+            packageName,
+            KEY_COLOR_SECONDARY,
+            KEY_COLOR_SECONDARY_DARK,
+            defaultColors.secondaryColor,
+            defaultColors.secondaryDarkColor);
+
+    return new CarAppColors(
+        primaryColorVariants.first,
+        primaryColorVariants.second,
+        secondaryColorVariants.first,
+        secondaryColorVariants.second);
+  }
+
+  /**
+   * Darkens the given color by a percentage of its brightness.
+   *
+   * @param originalColor the color to change the brightness of
+   * @param percentage the percentage to decrement the brightness for, in the [0..1] range. For
+   *     example, a value of 0.5 will make the color 50% less bright
+   */
+  @ColorInt
+  public static int darkenColor(@ColorInt int originalColor, float percentage) {
+    float[] hsv = new float[3];
+    Color.colorToHSV(originalColor, hsv);
+    hsv[2] *= 1.f - percentage;
+    return Color.HSVToColor(hsv);
+  }
+
+  /**
+   * Blends two colors using a SRC Porter-duff operator.
+   *
+   * <p>See <a href="http://ssp.impulsetrain.com/porterduff.html">Porter-Duff Compositing and Blend
+   * Modes</a>
+   *
+   * <p>NOTE: this function ignores the alpha channel of the destination, and returns a fully opaque
+   * color.
+   */
+  @ColorInt
+  public static int blendColorsSrc(@ColorInt int source, @ColorInt int destination) {
+    // Each color component is calculated like so:
+    // output_color = (1 - alpha(source)) * destination + alpha_source * source
+    float alpha = Color.alpha(source) / 255.f;
+    return Color.argb(
+        255,
+        clampComponent(alpha * Color.red(source) + (1 - alpha) * Color.red(destination)),
+        clampComponent(alpha * Color.green(source) + (1 - alpha) * Color.green(destination)),
+        clampComponent(alpha * Color.blue(source) + (1 - alpha) * Color.blue(destination)));
+  }
+
+  /**
+   * Checks whether the given colors provide an acceptable contrast ratio.
+   *
+   * <p>See <a href="https://material.io/design/usability/accessibility.html#color-and-contrast">
+   * Color and Contrast</a>
+   *
+   * <p>If {@code backgroundColor} is {@link Color#TRANSPARENT}, any {@code foregroundColor} will
+   * pass the check.
+   *
+   * @param foregroundColor the foreground color for which the contrast should be checked.
+   * @param backgroundColor the background color for which the contrast should be checked.
+   * @return true if placing the foreground color over the background color results in an acceptable
+   *     contrast.
+   */
+  public static boolean hasMinimumColorContrast(
+      @ColorInt int foregroundColor, @ColorInt int backgroundColor) {
+    if (backgroundColor == Color.TRANSPARENT) {
+      return true;
+    }
+
+    return calculateContrast(foregroundColor, backgroundColor) > MINIMUM_COLOR_CONTRAST;
+  }
+
+  /**
+   * Check if any variant in the given {@code foregroundCarColor} has enough color contrast against
+   * the given {@code backgroundColor}.
+   */
+  public static boolean checkColorContrast(
+      TemplateContext templateContext, CarColor foregroundCarColor, @ColorInt int backgroundColor) {
+    if (backgroundColor == Color.TRANSPARENT) {
+      return true;
+    }
+
+    if (CarColor.DEFAULT.equals(foregroundCarColor)) {
+      return true;
+    }
+
+    CarColor foregroundColor = convertToCustom(templateContext, foregroundCarColor);
+    boolean checkPasses =
+        hasMinimumColorContrast(foregroundColor.getColor(), backgroundColor)
+            || hasMinimumColorContrast(foregroundColor.getColorDark(), backgroundColor);
+    if (!checkPasses) {
+      L.w(
+          LogTags.TEMPLATE,
+          "Color contrast check failed, foreground car color: %s, background color: %d",
+          foregroundCarColor,
+          backgroundColor);
+      templateContext.getColorContrastCheckState().setCheckPassed(false);
+    }
+    return checkPasses;
+  }
+
+  /**
+   * Returns whether the icon's tint passes the color contrast check against the given background
+   * color.
+   */
+  public static boolean checkIconTintContrast(
+      TemplateContext templateContext, @Nullable CarIcon icon, @ColorInt int backgroundColor) {
+    boolean passes = true;
+    if (icon != null) {
+      CarColor iconTint = icon.getTint();
+      if (iconTint != null) {
+        passes = checkColorContrast(templateContext, iconTint, backgroundColor);
+      }
+    }
+    return passes;
+  }
+
+  /**
+   * Convert the given {@code carColor} into a {@link CarColor} of type {@link
+   * CarColor#TYPE_CUSTOM}.
+   */
+  private static CarColor convertToCustom(TemplateContext templateContext, CarColor carColor) {
+    if (carColor.getType() == TYPE_CUSTOM) {
+      return carColor;
+    }
+
+    @ColorInt
+    int color =
+        resolveColor(
+            templateContext,
+            carColor,
+            /* isDark= */ false,
+            Color.TRANSPARENT,
+            CarColorConstraints.UNCONSTRAINED);
+    @ColorInt
+    int colorDark =
+        resolveColor(
+            templateContext,
+            carColor,
+            /* isDark= */ true,
+            Color.TRANSPARENT,
+            CarColorConstraints.UNCONSTRAINED);
+    return CarColor.createCustom(color, colorDark);
+  }
+
+  /**
+   * Between the given {@code color} and {@code colorDark}, returns the color that has enough color
+   * contrast against the given {@code backgroundColor}.
+   *
+   * <p>If none of the given colors passes the check, returns {@code defaultColor}.
+   *
+   * <p>If {@code isDark} is {@code true}, {@code colorDark} will be checked first, otherwise {@code
+   * color} will be checked first. The first color passes the check will be returned.
+   */
+  @ColorInt
+  private static int getContrastCheckedColor(
+      @ColorInt int color,
+      @ColorInt int colorDark,
+      @ColorInt int backgroundColor,
+      @ColorInt int defaultColor,
+      boolean isDark) {
+    int[] colors = new int[2];
+    if (isDark) {
+      colors[0] = colorDark;
+      colors[1] = color;
+    } else {
+      colors[0] = color;
+      colors[1] = colorDark;
+    }
+
+    for (@ColorInt int col : colors) {
+      if (hasMinimumColorContrast(col, backgroundColor)) {
+        return col;
+      }
+    }
+    return defaultColor;
+  }
+
+  private static int clampComponent(float color) {
+    return (int) Math.max(0, Math.min(255, color));
+  }
+
+  private CarColorUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java
new file mode 100644
index 0000000..8bd4ac6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import static androidx.annotation.VisibleForTesting.PROTECTED;
+import static java.lang.Math.min;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.AppInfo;
+import androidx.car.app.versioning.CarAppApiLevels;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.StatusReporter;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/** Configuration options from the car host. */
+public abstract class CarHostConfig implements StatusReporter {
+
+  /** Represent the OEMs' preference on ordering the primary action */
+  @IntDef(
+      value = {
+        PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET,
+        PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT,
+        PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT,
+      })
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface PrimaryActionOrdering {}
+
+  /** Indicates that OEMs choose to not re-ordering the actions */
+  public static final int PRIMARY_ACTION_HORIZONTAL_ORDER_NOT_SET = 0;
+
+  /** Indicates that OEMs choose to put the primary action on the left */
+  public static final int PRIMARY_ACTION_HORIZONTAL_ORDER_LEFT = 1;
+
+  /** Indicates that OEMs choose to put the primary action on the right */
+  public static final int PRIMARY_ACTION_HORIZONTAL_ORDER_RIGHT = 2;
+
+  private final ComponentName mAppName;
+  // Default to oldest as the min communication until updated via a call to negotiateApi.
+  // The oldest is the default lowest common denominator for communication.
+  private int mNegotiatedApi = CarAppApiLevels.getOldest();
+  // Last received app info, used for debugging purposes. This is the information the above
+  // negotiated API level is based on.
+  @Nullable private AppInfo mAppInfo = null;
+
+  public CarHostConfig(ComponentName appName) {
+    mAppName = appName;
+  }
+
+  /**
+   * Returns how many seconds after the user leaves an app, should the system wait before unbinding
+   * from it.
+   */
+  public abstract int getAppUnbindSeconds();
+
+  /** Returns a list of intent extras to be stripped before binding to the client app. */
+  public abstract List<String> getHostIntentExtrasToRemove();
+
+  /** Returns whether the provided intent should be treated as a new task flow. */
+  public abstract boolean isNewTaskFlowIntent(Intent intent);
+
+  /**
+   * Updates the API level for communication between the host and the connecting app.
+   *
+   * @return the negotiated api
+   * @throws IncompatibleApiException if the app's supported API range does not work with the host's
+   *     API range
+   */
+  public int updateNegotiatedApi(AppInfo appInfo) throws IncompatibleApiException {
+    mAppInfo = appInfo;
+    int appMinApi = mAppInfo.getMinCarAppApiLevel();
+    int appMaxApi = mAppInfo.getLatestCarAppApiLevel();
+    int hostMinApi = getHostMinApi();
+    int hostMaxApi = getHostMaxApi();
+
+    L.i(
+        LogTags.APP_HOST,
+        "App: [%s] app info: [%s] Host min api: [%d]  Host max api: [%d]",
+        mAppName.flattenToShortString(),
+        mAppInfo,
+        hostMinApi,
+        hostMaxApi);
+
+    if (appMinApi > hostMaxApi) {
+      throw new IncompatibleApiException(
+          ApiIncompatibilityType.HOST_TOO_OLD,
+          "App required min API level ["
+              + appMinApi
+              + "] is higher than the host's max API level ["
+              + hostMaxApi
+              + "]");
+    } else if (hostMinApi > appMaxApi) {
+      throw new IncompatibleApiException(
+          ApiIncompatibilityType.APP_TOO_OLD,
+          "Host required min API level ["
+              + hostMinApi
+              + "] is higher than the app's max API level ["
+              + appMaxApi
+              + "]");
+    }
+
+    mNegotiatedApi = min(appMaxApi, hostMaxApi);
+    L.d(
+        LogTags.APP_HOST,
+        "App: [%s], Host negotiated api: [%d]",
+        mAppName.flattenToShortString(),
+        mNegotiatedApi);
+
+    return mNegotiatedApi;
+  }
+
+  /** Returns the {@link AppInfo} that was last set, or {@code null} otherwise. */
+  @Nullable
+  public AppInfo getAppInfo() {
+    return mAppInfo;
+  }
+
+  /**
+   * Returns the API that was negotiated between the host and the connecting app. The host should
+   * use this value to determine if a feature for a particular API is supported for the app.
+   */
+  public int getNegotiatedApi() {
+    return mNegotiatedApi;
+  }
+
+  @Override
+  public void reportStatus(PrintWriter pw, Pii piiHandling) {
+    pw.printf(
+        "- host min api: %d, host max api: %d, negotiated api: %s\n",
+        getHostMinApi(), getHostMaxApi(), mNegotiatedApi);
+    pw.printf(
+        "- app min api: %s, app target api: %s\n",
+        mAppInfo != null ? mAppInfo.getMinCarAppApiLevel() : "-",
+        mAppInfo != null ? mAppInfo.getLatestCarAppApiLevel() : "-");
+    pw.printf(
+        "- sdk version: %s\n", mAppInfo != null ? mAppInfo.getLibraryDisplayVersion() : "n/a");
+  }
+
+  /**
+   * Returns the host minimum API supported for the app.
+   *
+   * <p>Depending on the connecting app, the host may be configured to use a higher API level than
+   * the lowest level that the host is capable of supporting.
+   */
+  @VisibleForTesting(otherwise = PROTECTED)
+  public abstract int getHostMinApi();
+
+  /**
+   * Returns the host maximum API supported for the app.
+   *
+   * <p>Depending on the connecting app, the host may be configured to use a lower API level than
+   * the highest level that the host is capable of supporting.
+   */
+  @VisibleForTesting(otherwise = PROTECTED)
+  public abstract int getHostMaxApi();
+
+  /** Returns whether oem choose to ignore app provided colors on buttons on select templates. */
+  public abstract boolean isButtonColorOverriddenByOEM();
+
+  /**
+   * Returns the primary action order
+   *
+   * <p>Depending on the OEMs config, the primary action can be placed on the right or left,
+   * regardless of the config from connection app.
+   *
+   * @see PrimaryActionOrdering
+   */
+  @PrimaryActionOrdering
+  public abstract int getPrimaryActionOrder();
+
+  /** Returns true if the host supports cluster activity */
+  public abstract boolean isClusterEnabled();
+
+  /** Returns whether the host supports pan and zoom features in the navigation template */
+  public abstract boolean isNavPanZoomEnabled();
+
+  /** Returns whether the host supports pan and zoom features in POI and route preview templates */
+  public abstract boolean isPoiRoutePreviewPanZoomEnabled();
+
+  /** Returns whether the host supports pan and zoom features in POI and route preview templates */
+  public abstract boolean isPoiContentRefreshEnabled();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java
new file mode 100644
index 0000000..ff5b0e2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/**
+ * Manages the state of color contrast checks in template apps.
+ *
+ * <p>This class tracks the state for a single template in a single app.
+ */
+public interface ColorContrastCheckState {
+  /** Sets whether the color contrast check passed in the current template. */
+  void setCheckPassed(boolean passed);
+
+  /** Returns whether the color contrast check passed in the current template. */
+  boolean getCheckPassed();
+
+  /** Returns whether the host checks color contrast. */
+  // TODO(b/208683313): Remove once color contrast check is enabled in AAP
+  boolean checksContrast();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java
new file mode 100644
index 0000000..6356664
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.util.Pair;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/** Utility class to load a car app's primary and secondary colors. */
+public final class ColorUtils {
+  private static final String KEY_THEME = "androidx.car.app.theme";
+
+  // LINT.IfChange(car_colors)
+  public static final String KEY_COLOR_PRIMARY = "carColorPrimary";
+  public static final String KEY_COLOR_PRIMARY_DARK = "carColorPrimaryDark";
+  public static final String KEY_COLOR_SECONDARY = "carColorSecondary";
+  public static final String KEY_COLOR_SECONDARY_DARK = "carColorSecondaryDark";
+  // LINT.ThenChange()
+
+  private ColorUtils() {}
+
+  /** Returns a {@link Context} set up for the given package. */
+  @Nullable
+  public static Context getPackageContext(Context context, String packageName) {
+    Context packageContext;
+    try {
+      packageContext = context.createPackageContext(packageName, /* flags= */ 0);
+    } catch (PackageManager.NameNotFoundException e) {
+      L.e(LogTags.APP_HOST, e, "Package %s does not exist", packageName);
+      return null;
+    }
+    return packageContext;
+  }
+
+  /**
+   * Returns the ID of the theme to use for the app described by the given component name.
+   *
+   * <p>This theme id is used to load custom primary and secondary colors from the remote app.
+   *
+   * @see com.google.android.libraries.car.app.model.CarColor
+   */
+  @StyleRes
+  public static int loadThemeId(Context context, ComponentName componentName) {
+    int theme = 0;
+    ServiceInfo serviceInfo = getServiceInfo(context, componentName);
+    if (serviceInfo != null && serviceInfo.metaData != null) {
+      theme = serviceInfo.metaData.getInt(KEY_THEME);
+    }
+
+    // If theme is not specified in service information, fallback to KEY_THEME in application
+    // info.
+    if (theme == 0) {
+      ApplicationInfo applicationInfo = getApplicationInfo(context, componentName);
+      if (applicationInfo != null) {
+        if (applicationInfo.metaData != null) {
+          theme = applicationInfo.metaData.getInt(KEY_THEME);
+        }
+        // If no override provided in service and application info, fallback to default app
+        // theme.
+        if (theme == 0) {
+          theme = applicationInfo.theme;
+        }
+      }
+    }
+
+    return theme;
+  }
+
+  /**
+   * Returns the color values for the given light and dark variants.
+   *
+   * <p>If a variant is not specified in the theme, default values are returned for both variants.
+   */
+  public static Pair<Integer, Integer> getColorVariants(
+      Resources.Theme appTheme,
+      String packageName,
+      String colorKey,
+      String darkColorKey,
+      @ColorInt int defaultColor,
+      @ColorInt int defaultDarkColor) {
+    Resources appResources = appTheme.getResources();
+    int colorId = appResources.getIdentifier(colorKey, "attr", packageName);
+    int darkColorId = appResources.getIdentifier(darkColorKey, "attr", packageName);
+
+    // If light or dark variant is not specified, return default variants.
+    if (colorId == Resources.ID_NULL || darkColorId == Resources.ID_NULL) {
+      return new Pair<>(defaultColor, defaultDarkColor);
+    }
+
+    @ColorInt int color = getColor(colorId, /* defaultColor= */ Color.TRANSPARENT, appTheme);
+    @ColorInt
+    int darkColor = getColor(darkColorId, /* defaultColor= */ Color.TRANSPARENT, appTheme);
+
+    // Even if the resource ID exists for a variant, it may not have a value. If so, use default
+    // variants.
+    if (color == Color.TRANSPARENT || darkColor == Color.TRANSPARENT) {
+      return new Pair<>(defaultColor, defaultDarkColor);
+    }
+    return new Pair<>(color, darkColor);
+  }
+
+  /** Returns the color specified by the given resource id from the given app theme. */
+  @ColorInt
+  private static int getColor(int resId, @ColorInt int defaultColor, Resources.Theme appTheme) {
+    @ColorInt int color = defaultColor;
+    if (resId != Resources.ID_NULL) {
+      int[] attr = {resId};
+      TypedArray ta = appTheme.obtainStyledAttributes(attr);
+      color = ta.getColor(0, defaultColor);
+      ta.recycle();
+    }
+    return color;
+  }
+
+  @Nullable
+  private static ServiceInfo getServiceInfo(Context context, ComponentName componentName) {
+    try {
+      return context
+          .getPackageManager()
+          .getServiceInfo(componentName, PackageManager.GET_META_DATA);
+    } catch (PackageManager.NameNotFoundException e) {
+      L.e(LogTags.APP_HOST, e, "Component %s doesn't exist", componentName);
+    }
+
+    return null;
+  }
+
+  @Nullable
+  private static ApplicationInfo getApplicationInfo(Context context, ComponentName componentName) {
+    try {
+      return context
+          .getPackageManager()
+          .getApplicationInfo(componentName.getPackageName(), PackageManager.GET_META_DATA);
+    } catch (PackageManager.NameNotFoundException e) {
+      L.e(LogTags.APP_HOST, e, "Package %s doesn't exist", componentName.getPackageName());
+    }
+
+    return null;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java
new file mode 100644
index 0000000..8733cf7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.res.Configuration;
+import android.widget.Toast;
+import androidx.car.app.model.OnClickDelegate;
+
+/** Holds static util methods for common usage in the host. */
+public final class CommonUtils {
+
+  /**
+   * Checks if {@code onClickDelegate} is a parked only action and the car is driving, then shows a
+   * toast and returns. Otherwise dispatches the {@code onClick} to the client.
+   */
+  public static void dispatchClick(
+      TemplateContext templateContext, OnClickDelegate onClickDelegate) {
+    if (onClickDelegate.isParkedOnly()
+        && templateContext.getConstraintsProvider().isConfigRestricted()) {
+      templateContext
+          .getToastController()
+          .showToast(
+              templateContext
+                  .getResources()
+                  .getString(templateContext.getHostResourceIds().getParkedOnlyActionText()),
+              Toast.LENGTH_SHORT);
+      return;
+    }
+    templateContext.getAppDispatcher().dispatchClick(onClickDelegate);
+  }
+
+  /** Returns {@code true} if the host is in dark mode, {@code false} otherwise. */
+  public static boolean isDarkMode(TemplateContext templateContext) {
+    Configuration configuration = templateContext.getResources().getConfiguration();
+    return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
+        == Configuration.UI_MODE_NIGHT_YES;
+  }
+
+  private CommonUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java
new file mode 100644
index 0000000..44b02d2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import androidx.annotation.MainThread;
+import androidx.car.app.model.TemplateWrapper;
+
+/**
+ * The interface for forwarding custom debug overlay information to the host fragment or activity.
+ */
+@MainThread
+public interface DebugOverlayHandler {
+  /**
+   * Returns {@code true} if the debug overlay is active.
+   *
+   * <p>The caller can use the active state to determine whether to process debug overlay
+   * information or not.
+   */
+  boolean isActive();
+
+  /**
+   * Sets debug overlay as active/inactive if parameter is {@code true}/{@code false} respectively.
+   */
+  void setActive(boolean active);
+
+  /** Clears all existing debug overlay. */
+  void clearAllEntries();
+
+  /**
+   * Removes the debug overlay entry associated with the input {@code debugKey}.
+   *
+   * <p>If the {@code debugKey} is not associated with any existing entry, this call is a no-op.
+   */
+  void removeDebugOverlayEntry(String debugKey);
+
+  /**
+   * Updates the debug overlay entry associated with a given {@code debugKey}.
+   *
+   * <p>This would override any previous debug text for the same key.
+   */
+  void updateDebugOverlayEntry(String debugKey, String debugOverlayText);
+
+  /** Returns text to render for debug overlay. */
+  CharSequence getDebugOverlayText();
+
+  /** Resets debug overlay with new information from {@link TemplateWrapper} */
+  void resetTemplateDebugOverlay(TemplateWrapper templateWrapper);
+
+  /** Set {@link Observer} for this {@link DebugOverlayHandler} */
+  void setObserver(Observer observer);
+
+  /**
+   * The interface that lets an object observe changes to the {@link DebugOverlayHandler}'s entries.
+   */
+  interface Observer {
+    void entriesUpdated();
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java
new file mode 100644
index 0000000..fc30645
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+/**
+ * Handles error cases, allowing classes that do not handle ui to be able to display an error screen
+ * to the user.
+ */
+public interface ErrorHandler {
+  /** Displays the given an error screen to the user. */
+  void showError(CarAppError error);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java
new file mode 100644
index 0000000..51aafee
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import static androidx.car.app.model.CarIcon.ERROR;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.OnClickListener;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/**
+ * Formats {@link CarAppError} into {@link MessageTemplate} to allow displaying the error to the
+ * user.
+ */
+public class ErrorMessageTemplateBuilder {
+  private final Context mContext;
+  private final HostResourceIds mHostResourceIds;
+  private final CarAppError mError;
+  private final ComponentName mAppName;
+  private final OnClickListener mMainActionOnClickListener;
+
+  private String mAppLabel;
+
+  /** Constructor of an {@link ErrorMessageTemplateBuilder} */
+  @SuppressWarnings("nullness")
+  public ErrorMessageTemplateBuilder(
+      @NonNull Context context,
+      @NonNull CarAppError error,
+      @NonNull HostResourceIds instance,
+      @NonNull OnClickListener listener) {
+
+    if (context == null || error == null || instance == null || listener == null) {
+      throw new NullPointerException();
+    }
+
+    mContext = context;
+    mError = error;
+    mAppName = error.getAppName();
+    mHostResourceIds = instance;
+    mMainActionOnClickListener = listener;
+  }
+
+  /** Returns an {@link ErrorMessageTemplateBuilder} with {@link String} appLabel */
+  @NonNull
+  public ErrorMessageTemplateBuilder setAppLabel(String appLabel) {
+    mAppLabel = appLabel;
+    return this;
+  }
+
+  /** Returns a {@link MessageTemplate} with error message */
+  public MessageTemplate build() {
+    if (mAppLabel == null) {
+      PackageManager pm = mContext.getPackageManager();
+      ApplicationInfo applicationInfo = null;
+      try {
+        applicationInfo = pm.getApplicationInfo(mAppName.getPackageName(), 0);
+      } catch (NameNotFoundException e) {
+        L.e(LogTags.TEMPLATE, e, "Could not find the application info");
+      }
+      mAppLabel =
+          applicationInfo == null
+              ? mAppName.getPackageName()
+              : pm.getApplicationLabel(applicationInfo).toString();
+    }
+    String errorMessage = getErrorMessage(mAppLabel, mError);
+    if (errorMessage == null) {
+      errorMessage = mContext.getString(mHostResourceIds.getClientErrorText(), mAppLabel);
+    }
+
+    // TODO(b/179320446): Note that we use a whitespace as the title to not show anything in
+    // the header. We will have to update this to some internal-only template once the
+    // whitespace string no longer supperted.
+    MessageTemplate.Builder messageTemplateBuilder =
+        new MessageTemplate.Builder(errorMessage).setTitle(" ").setIcon(ERROR);
+
+    Throwable cause = mError.getCause();
+    if (cause != null) {
+      messageTemplateBuilder.setDebugMessage(cause);
+    }
+
+    String debugMessage = mError.getDebugMessage();
+    if (debugMessage != null) {
+      messageTemplateBuilder.setDebugMessage(debugMessage);
+    }
+
+    messageTemplateBuilder.addAction(
+        new Action.Builder()
+            .setTitle(mContext.getString(mHostResourceIds.getExitText()))
+            .setOnClickListener(mMainActionOnClickListener)
+            .build());
+
+    Action extraAction = getExtraAction(mError.getType(), mError.getExtraAction());
+    if (extraAction != null) {
+      messageTemplateBuilder.addAction(extraAction);
+    }
+
+    return messageTemplateBuilder.build();
+  }
+
+  @Nullable
+  private String getErrorMessage(String appLabel, @Nullable CarAppError error) {
+    CarAppError.Type type = error == null ? null : error.getType();
+    if (error == null || type == null) {
+      return null;
+    }
+    switch (type) {
+      case ANR_TIMEOUT:
+        return mContext.getString(mHostResourceIds.getAnrMessage(), appLabel);
+      case ANR_WAITING:
+        return mContext.getString(mHostResourceIds.getAnrWaiting());
+      case INCOMPATIBLE_CLIENT_VERSION:
+        ApiIncompatibilityType apiIncompatibilityType = ApiIncompatibilityType.HOST_TOO_OLD;
+        Throwable exception = error.getCause();
+        if (exception instanceof IncompatibleApiException) {
+          apiIncompatibilityType = ((IncompatibleApiException) exception).getIncompatibilityType();
+        }
+        return mContext.getString(
+            mHostResourceIds.getAppApiIncompatibleText(apiIncompatibilityType), appLabel);
+      case MISSING_PERMISSION:
+        return mContext.getString(mHostResourceIds.getMissingPermissionText(), appLabel);
+    }
+    throw new IllegalArgumentException("Unknown error type: " + type);
+  }
+
+  @Nullable
+  private Action getExtraAction(@Nullable CarAppError.Type type, @Nullable Runnable extraAction) {
+    if (type != CarAppError.Type.ANR_TIMEOUT || extraAction == null) {
+      return null;
+    }
+    return new Action.Builder()
+        .setTitle(mContext.getString(mHostResourceIds.getAnrWait()))
+        .setOnClickListener(extraAction::run)
+        .build();
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java
new file mode 100644
index 0000000..b9a78f5
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.res.Resources;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.WeakHashMap;
+
+/** Handles event dispatch and subscription. */
+public class EventManager {
+  /** The type of events. */
+  public enum EventType {
+    /** Unknown event type */
+    UNKNOWN,
+
+    /** Signifies that the visible area of the view has changed. */
+    SURFACE_VISIBLE_AREA,
+
+    /** Signifies that the stable area of the view has changed. */
+    SURFACE_STABLE_AREA,
+
+    /**
+     * Signifies that one of the descendants of the template view hierarchy has been interacted
+     * with.
+     */
+    TEMPLATE_TOUCHED_OR_FOCUSED,
+
+    /** Signifies that the focus state of the window that contains the template view has changed. */
+    WINDOW_FOCUS_CHANGED,
+
+    /** Signifies that the Car UX Restrictions constraints on the template view have changed. */
+    CONSTRAINTS,
+
+    /**
+     * Signifies that the configuration of the view has changed.
+     *
+     * <p>The most up-to-date configuration can be retrieved via {@link
+     * Resources#getConfiguration()}
+     */
+    CONFIGURATION_CHANGED,
+
+    /** Signifies that the app is now unbound. */
+    APP_UNBOUND,
+
+    /** Signifies that the app has disconnected and will be rebound. */
+    APP_DISCONNECTED,
+
+    /**
+     * Signifies that the current list of places has changed.
+     *
+     * <p>This is used by the PlaceListMapTemplate to synchronize places between the list and the
+     * map views.
+     */
+    PLACE_LIST,
+
+    /** Signifies that WindowInsets has changed. */
+    WINDOW_INSETS,
+  }
+
+  // A weak-referenced map is used here so that subscribers do not have to explicitly unsubscribe
+  // themselves.
+  private final WeakHashMap<Object, List<Dependency>> mDependencyMap = new WeakHashMap<>();
+
+  /**
+   * Subscribes to an {@link EventType} and trigger the given {@link Runnable} when the event is
+   * fired.
+   *
+   * <p>The input weakReference instance should be used to associate and clean up the {@link
+   * Runnable} so that the event subscriber will automatically unsubscribe itself when the
+   * weak-referenced object is GC'd. However, if earlier un-subscription is preferred, {@link
+   * #unsubscribeEvent} can be called instead.
+   */
+  public void subscribeEvent(Object weakReference, EventType eventType, Runnable runnable) {
+    List<Dependency> objectDependencies = mDependencyMap.get(weakReference);
+    if (objectDependencies == null) {
+      objectDependencies = new ArrayList<>();
+      mDependencyMap.put(weakReference, objectDependencies);
+    }
+    objectDependencies.add(new Dependency(eventType, runnable));
+  }
+
+  /** Unsubscribes the given object (weakReference) to a certain {@link EventType}. */
+  public void unsubscribeEvent(Object weakReference, EventType eventType) {
+    List<Dependency> objectDependencies = mDependencyMap.get(weakReference);
+    if (objectDependencies != null) {
+      Iterator<Dependency> itr = objectDependencies.iterator();
+      while (itr.hasNext()) {
+        Dependency dependency = itr.next();
+        if (dependency.mEventType == eventType) {
+          itr.remove();
+        }
+      }
+    }
+  }
+
+  /** Dispatches the given {@link EventType} so subscriber can react to it. */
+  public void dispatchEvent(EventType eventType) {
+    // TODO(b/163634344): Avoid creating a temp collection. This is needed to prevent concurrent
+    // modifications that could happen if something subscribe to an event while
+    // listening/handling
+    // an existing event.
+    Collection<List<Dependency>> dependencySet = new ArrayList<>(mDependencyMap.values());
+    for (List<Dependency> dependencies : dependencySet) {
+      for (Dependency dependency : dependencies) {
+        if (dependency.mEventType == eventType) {
+          dependency.mRunnable.run();
+        }
+      }
+    }
+  }
+
+  /** An internal container for associating an {@link EventType} with a {@link Runnable}. */
+  private static class Dependency {
+    private final EventType mEventType;
+    private final Runnable mRunnable;
+
+    Dependency(EventType eventType, Runnable runnable) {
+      mEventType = eventType;
+      mRunnable = runnable;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java
new file mode 100644
index 0000000..4bc2210
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
+/**
+ * Host-dependent resource identifiers.
+ *
+ * <p>Given that each host will have its own set of resources, this interface abstracts out the
+ * exact resource needed in each case.
+ */
+public interface HostResourceIds {
+
+  /** Returns the resource ID of drawable for the alert icon. */
+  @DrawableRes
+  int getAlertIconDrawable();
+
+  /** Returns the resource ID of the drawable for the error icon. */
+  @DrawableRes
+  int getErrorIconDrawable();
+
+  /** Returns the resource ID of the drawable for the error icon. */
+  @DrawableRes
+  int getBackIconDrawable();
+
+  /** Returns the resource ID of the drawable for the pan icon. */
+  @DrawableRes
+  int getPanIconDrawable();
+
+  /** Returns the resource ID of drawable for the refresh icon. */
+  @DrawableRes
+  int getRefreshIconDrawable();
+
+  /** Returns the resource ID of the standard red color. */
+  @ColorRes
+  int getRedColor();
+
+  /** Returns the resource ID of the standard red color's dark variant. */
+  @ColorRes
+  int getRedDarkColor();
+
+  /** Returns the resource ID of the standard green color. */
+  @ColorRes
+  int getGreenColor();
+
+  /** Returns the resource ID of the standard red color's dark variant. */
+  @ColorRes
+  int getGreenDarkColor();
+
+  /** Returns the resource ID of the standard blue color. */
+  @ColorRes
+  int getBlueColor();
+
+  /** Returns the resource ID of the standard red color's dark variant. */
+  @ColorRes
+  int getBlueDarkColor();
+
+  /** Returns the resource ID of the standard yellow color. */
+  @ColorRes
+  int getYellowColor();
+
+  /** Returns the resource ID of the standard red color's dark variant. */
+  @ColorRes
+  int getYellowDarkColor();
+
+  /**
+   * Returns the resource ID of the default color to use for the standard primary color, unless
+   * specified by the app.
+   */
+  @ColorRes
+  int getDefaultPrimaryColor();
+
+  /**
+   * Returns the resource ID of the default color to use for the standard primary color, unless
+   * specified by the app, in its dark variant.
+   */
+  @ColorRes
+  int getDefaultPrimaryDarkColor();
+
+  /**
+   * Returns the resource ID of the default color to use for the standard secondary color, unless
+   * specified by the app.
+   */
+  @ColorRes
+  int getDefaultSecondaryColor();
+
+  /**
+   * Returns the resource ID of the default color to use for the standard secondary color, unless
+   * specified by the app, in its dark variant.
+   */
+  @ColorRes
+  int getDefaultSecondaryDarkColor();
+
+  /** Returns the resource ID of the string used to format a distance in meters. */
+  @StringRes
+  int getDistanceInMetersStringFormat();
+
+  /** Returns the resource ID of the string used to format a distance in kilometers. */
+  @StringRes
+  int getDistanceInKilometersStringFormat();
+
+  /** Returns the resource ID of the string used to format a distance in feet. */
+  @StringRes
+  int getDistanceInFeetStringFormat();
+
+  /** Returns the resource ID of the string used to format a distance in miles. */
+  @StringRes
+  int getDistanceInMilesStringFormat();
+
+  /** Returns the resource ID of the string used to format a distance in yards. */
+  @StringRes
+  int getDistanceInYardsStringFormat();
+
+  /** Returns the resource ID of the string used to format a time with a time zone string. */
+  @StringRes
+  int getTimeAtDestinationWithTimeZoneStringFormat();
+
+  /** Returns the resource ID of the string used to format a duration in days. */
+  @StringRes
+  int getDurationInDaysStringFormat();
+
+  /** Returns the resource ID of the string used to format a duration in days and hours. */
+  @StringRes
+  int getDurationInDaysAndHoursStringFormat();
+
+  /** Returns the resource ID of the string used to format a duration in hours. */
+  @StringRes
+  int getDurationInHoursStringFormat();
+
+  /** Returns the resource ID of the string used to format a duration in hours and minutes. */
+  @StringRes
+  int getDurationInHoursAndMinutesStringFormat();
+
+  /** Returns the resource ID of the string used to format a duration in minutes. */
+  @StringRes
+  int getDurationInMinutesStringFormat();
+
+  /** Returns the resource ID of the error message for client app exception */
+  @StringRes
+  int getAnrMessage();
+
+  /** Returns the resource ID of the button text for waiting for ANR */
+  @StringRes
+  int getAnrWait();
+
+  /** Returns the resource ID of the error message for waiting for application to respond */
+  @StringRes
+  int getAnrWaiting();
+
+  /**
+   * Returns the resource ID of the error message for client version check failure of the given
+   * {@link ApiIncompatibilityType}
+   */
+  @StringRes
+  int getAppApiIncompatibleText(@NonNull ApiIncompatibilityType apiIncompatibilityType);
+
+  /** Returns the resource ID of the error message for client app exception */
+  @StringRes
+  int getClientErrorText();
+
+  /**
+   * Returns the resource ID of the error message for the application not having required permission
+   */
+  @StringRes
+  int getMissingPermissionText();
+
+  /** Returns the resource ID of the error message for client app exception */
+  @StringRes
+  int getExitText();
+
+  /**
+   * Returns the resource ID of the toast message for user selecting action that can only be
+   * selected when parked
+   */
+  @StringRes
+  int getParkedOnlyActionText();
+
+  /** Returns the resource ID of the search hint */
+  @StringRes
+  int getSearchHintText();
+
+  /** Returns the resource ID of the disabled search hint */
+  @StringRes
+  int getSearchHintDisabledText();
+
+  /** Returns the resource ID of the message for driving state */
+  @StringRes
+  int getDrivingStateMessageText();
+
+  /** Returns the resource ID of the message for no item for the current list */
+  @StringRes
+  int getTemplateListNoItemsText();
+
+  /** Returns the resource ID of the message for disabled action in long message template */
+  @StringRes
+  int getLongMessageTemplateDisabledActionText();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java
new file mode 100644
index 0000000..e0629f6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/** An exception for API incompatibility between the host and the connecting app. */
+public final class IncompatibleApiException extends Exception {
+
+  private final ApiIncompatibilityType mApiIncompatibilityType;
+
+  public IncompatibleApiException(ApiIncompatibilityType apiIncompatibilityType, String message) {
+    super(message);
+    mApiIncompatibilityType = apiIncompatibilityType;
+  }
+
+  public ApiIncompatibilityType getIncompatibilityType() {
+    return mApiIncompatibilityType;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java
new file mode 100644
index 0000000..d45618e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import java.util.List;
+
+/** Holds static util methods for host Intent manipulations. */
+public final class IntentUtils {
+  private IntentUtils() {}
+
+  private static final String EXTRA_ORIGINAL_INTENT_KEY =
+      "com.android.car.libraries.apphost.common.ORIGINAL_INTENT";
+
+  /** Embeds {@code originalIntent} inside {@code wrappingIntent} for later extraction. */
+  public static void embedOriginalIntent(
+      @NonNull Intent wrappingIntent, @NonNull Intent originalIntent) {
+    wrappingIntent.putExtra(EXTRA_ORIGINAL_INTENT_KEY, originalIntent);
+  }
+
+  /**
+   * Tries to extract the embedded "original" intent. Gearhead doesn't set this, so it won't always
+   * be there.
+   */
+  @NonNull
+  public static Intent extractOriginalIntent(@NonNull Intent binderIntent) {
+    Intent originalIntent = binderIntent.getParcelableExtra(EXTRA_ORIGINAL_INTENT_KEY);
+    return originalIntent != null ? originalIntent : binderIntent;
+  }
+
+  /**
+   * Removes any extras that we pass around internally as metadata, preventing them from being
+   * exposed to the client apps.
+   */
+  public static void removeInternalIntentExtras(Intent intent, List<String> extrasToRemove) {
+    for (String extra : extrasToRemove) {
+      intent.removeExtra(extra);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java
new file mode 100644
index 0000000..90aeda8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/** An exception denoting that the car host was accessed after it has become invalidated */
+public class InvalidatedCarHostException extends IllegalStateException {
+  /** Constructs a {@link InvalidatedCarHostException} instance. */
+  public InvalidatedCarHostException(String message) {
+    super(message);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java
new file mode 100644
index 0000000..931d5f1
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.location.Location;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.Place;
+import java.util.List;
+
+/**
+ * A mediator for communicating {@link Place}s, and related information, from and to different
+ * components in the UI hierarchy.
+ */
+public interface LocationMediator extends AppHostService {
+  /**
+   * Listener for notifying location changes by the app.
+   *
+   * <p>We do not go through the EventManager because we need to keep track of the listeners that
+   * are registered so we know when to start and stop requesting location updates from the app.
+   */
+  interface AppLocationListener {
+    void onAppLocationChanged(Location location);
+  }
+
+  /** Returns the current set of places of interest, or an empty list if there are none. */
+  List<Place> getCurrentPlaces();
+
+  /** Set a new list of places. */
+  void setCurrentPlaces(List<Place> places);
+
+  /** Returns the point when the camera was last anchored, or {@code null} if there was none. */
+  @Nullable
+  CarLocation getCameraAnchor();
+
+  /** Set the center point of where the camera is anchored, or {@code null} if it is unknown. */
+  void setCameraAnchor(@Nullable CarLocation cameraAnchor);
+
+  /**
+   * Add a listener for getting app location updates.
+   *
+   * <p>Note that using this on {@link androidx.car.app.versioning.CarAppApiLevel} 3 or lower would
+   * have no effect.
+   */
+  void addAppLocationListener(AppLocationListener listener);
+
+  /**
+   * Removes the listener which stops it from receiving app location updates.
+   *
+   * <p>Note that using this on {@link androidx.car.app.versioning.CarAppApiLevel} 3 or lower would
+   * have no effect.
+   */
+  void removeAppLocationListener(AppLocationListener listener);
+
+  /**
+   * Sets the {@link Location} as provided by the app.
+   *
+   * <p>This will notify the {@link AppLocationListener} that have been registered.
+   */
+  void setAppLocation(Location location);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java
new file mode 100644
index 0000000..fabef46
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+/** Gesture manager that handles gestures in map-based template presenters. */
+public class MapGestureManager {
+  /** The minimum span value for the scale event. */
+  private static final int MIN_SCALE_SPAN_DP = 10;
+
+  private final ScaleGestureDetector mScaleGestureDetector;
+  private final GestureDetector mGestureDetector;
+  private final MapOnGestureListener mGestureListener;
+
+  public MapGestureManager(TemplateContext templateContext, long touchUpdateThresholdMillis) {
+    Handler touchHandler = new Handler(Looper.getMainLooper());
+    mGestureListener = new MapOnGestureListener(templateContext, touchUpdateThresholdMillis);
+    mScaleGestureDetector =
+        new ScaleGestureDetector(
+            templateContext, mGestureListener, touchHandler, MIN_SCALE_SPAN_DP);
+    mGestureDetector = new GestureDetector(templateContext, mGestureListener, touchHandler);
+  }
+
+  /** Handles the gesture from the given motion event. */
+  public void handleGesture(MotionEvent event) {
+    mScaleGestureDetector.onTouchEvent(event);
+    if (!mScaleGestureDetector.isInProgress()) {
+      mGestureDetector.onTouchEvent(event);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java
new file mode 100644
index 0000000..ef13bad
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import com.android.car.libraries.apphost.common.ScaleGestureDetector.OnScaleGestureListener;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import java.text.DecimalFormat;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Gesture listener in map-based template presenters.
+ *
+ * <p>The following events are rate-limited to reduce the delay between touch gestures and the app
+ * response:
+ *
+ * <ul>
+ *   <li>{@link #onScroll(MotionEvent, MotionEvent, float, float)}
+ *   <li>{@link #onScale(ScaleGestureDetector)}
+ * </ul>
+ */
+public class MapOnGestureListener extends SimpleOnGestureListener
+    implements OnScaleGestureListener {
+  /** Maximum number of debug overlay texts to display. */
+  private static final int MAX_DEBUG_OVERLAY_LINES = 3;
+
+  /** The scale factor to send to the app when the user double taps on the screen. */
+  private static final float DOUBLE_TAP_ZOOM_FACTOR = 2f;
+
+  private final DecimalFormat mDecimalFormat = new DecimalFormat("#.##");
+
+  private final Deque<String> mDebugOverlayTexts = new ArrayDeque<>();
+
+  private final TemplateContext mTemplateContext;
+
+  /** The time threshold between touch events. */
+  private final long mTouchUpdateThresholdMillis;
+
+  /** The last time that a scroll touch event happened. */
+  private long mScrollLastTouchTimeMillis;
+
+  /** The last time that a scale touch event happened. */
+  private long mScaleLastTouchTimeMillis;
+
+  /** The scroll distance in the X axis since the last distance update to the car app. */
+  private float mCumulativeDistanceX;
+
+  /** The scroll distance in the Y axis since the last distance update to the car app. */
+  private float mCumulativeDistanceY;
+
+  /**
+   * A flag that indicates that the scale gesture just ended.
+   *
+   * <p>This flag is used to work around the issue where a fling gesture is detected when a scale
+   * event ends.
+   */
+  private boolean mScaleJustEnded;
+
+  /** A flag that indicates that user is currently scrolling. */
+  private boolean mIsScrolling;
+
+  public MapOnGestureListener(TemplateContext templateContext, long touchUpdateThresholdMillis) {
+    this.mTemplateContext = templateContext;
+    this.mTouchUpdateThresholdMillis = touchUpdateThresholdMillis;
+  }
+
+  @Override
+  public boolean onDown(MotionEvent e) {
+    L.d(LogTags.TEMPLATE, "Down touch event detected");
+    // Reset the flag that indicates that a sequence of scroll events may be starting from this
+    // point.
+    mIsScrolling = false;
+
+    mCumulativeDistanceX = 0;
+    mCumulativeDistanceY = 0;
+    return super.onDown(e);
+  }
+
+  @Override
+  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+    long touchTimeMillis = SystemClock.uptimeMillis();
+
+    // If this is the first scroll event in a series of gestures, log a telemetry event.
+    // This avoids triggering more than one event per sequence from finger touching down to
+    // finger lifted off the screen.
+    if (!mIsScrolling) {
+      // Since this is essentially the beginning of the scroll gesture, we need to check if
+      // SurfaceCallbackHandler allows the scroll to begin (e.g. checking against whether the
+      // user is already interacting with the screen too often).
+      SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+      if (!handler.canStartNewGesture()) {
+        mCumulativeDistanceX = 0;
+        mCumulativeDistanceY = 0;
+        return true;
+      }
+
+      mIsScrolling = true;
+      mTemplateContext
+          .getTelemetryHandler()
+          .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.PAN));
+    }
+
+    mCumulativeDistanceX += distanceX;
+    mCumulativeDistanceY += distanceY;
+
+    if (touchTimeMillis - mScrollLastTouchTimeMillis > mTouchUpdateThresholdMillis) {
+      mTemplateContext
+          .getSurfaceCallbackHandler()
+          .onScroll(mCumulativeDistanceX, mCumulativeDistanceY);
+      mScrollLastTouchTimeMillis = touchTimeMillis;
+
+      // Reset the cumulative distance.
+      mCumulativeDistanceX = 0;
+      mCumulativeDistanceY = 0;
+
+      if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+        updateDebugOverlay(
+            "scroll distance [X: "
+                + mDecimalFormat.format(distanceX)
+                + ", Y: "
+                + mDecimalFormat.format(distanceY)
+                + "]");
+      }
+    }
+
+    return true;
+  }
+
+  @Override
+  public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+    // Do not send fling events when the scale event just ended. This works around the issue
+    // where a fling gesture is detected when a scale event ends.
+    if (!mScaleJustEnded) {
+      // Note that unlike the scroll, scale and double-tap events, onFling happens at the end of
+      // scroll events, so we do not check against SurfaceCallbackHandler#canStartNewGesture.
+      mTemplateContext.getSurfaceCallbackHandler().onFling(velocityX, velocityY);
+
+      if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+        updateDebugOverlay(
+            "fling velocity [X: "
+                + mDecimalFormat.format(velocityX)
+                + ", Y: "
+                + mDecimalFormat.format(velocityY)
+                + "]");
+      }
+
+      mTemplateContext
+          .getTelemetryHandler()
+          .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.FLING));
+    } else {
+      mScaleJustEnded = false;
+    }
+    return true;
+  }
+
+  @Override
+  public boolean onDoubleTap(MotionEvent e) {
+    SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+    if (!handler.canStartNewGesture()) {
+      return false;
+    }
+
+    float x = e.getX();
+    float y = e.getY();
+
+    // We cannot reliably map the touch pad position to the screen position.
+    // If the double tap happened in a touch pad, zoom into the center of the surface.
+    if (e.getSource() == InputDevice.SOURCE_TOUCHPAD) {
+      Rect visibleArea = mTemplateContext.getSurfaceInfoProvider().getVisibleArea();
+      if (visibleArea != null) {
+        x = visibleArea.centerX();
+        y = visibleArea.centerY();
+      } else {
+        // If we do not know the visible area, send negative focal point values to indicate
+        // that it is unavailable.
+        x = -1;
+        y = -1;
+      }
+    }
+
+    handler.onScale(x, y, DOUBLE_TAP_ZOOM_FACTOR);
+
+    if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+      updateDebugOverlay(
+          "scale focus [X: "
+              + mDecimalFormat.format(x)
+              + ", Y: "
+              + mDecimalFormat.format(y)
+              + "], factor ["
+              + DOUBLE_TAP_ZOOM_FACTOR
+              + "]");
+    }
+
+    mTemplateContext
+        .getTelemetryHandler()
+        .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.ZOOM));
+
+    return true;
+  }
+
+  @Override
+  public boolean onScale(ScaleGestureDetector detector) {
+    long touchTimeMillis = SystemClock.uptimeMillis();
+    boolean shouldSendScaleEvent =
+        touchTimeMillis - mScaleLastTouchTimeMillis > mTouchUpdateThresholdMillis;
+    if (shouldSendScaleEvent) {
+      handleScale(detector);
+      mScaleLastTouchTimeMillis = touchTimeMillis;
+    }
+
+    // If we return false here, the detector will continue accumulating the scale factor until
+    // the next time we return true.
+    return shouldSendScaleEvent;
+  }
+
+  @Override
+  public boolean onScaleBegin(ScaleGestureDetector detector) {
+    // We need to check if SurfaceCallbackHandler allows the scaling gesture to begin (e.g. checking
+    // against whether the user is already interacting with the screen too often). Returning false
+    // here if needed to tell the detector to ignore the rest of the gesture.
+    SurfaceCallbackHandler handler = mTemplateContext.getSurfaceCallbackHandler();
+    return handler.canStartNewGesture();
+  }
+
+  @Override
+  public void onScaleEnd(ScaleGestureDetector detector) {
+    handleScale(detector);
+    mScaleJustEnded = true;
+
+    mTemplateContext
+        .getTelemetryHandler()
+        .logCarAppTelemetry(TelemetryEvent.newBuilder(UiAction.ZOOM));
+  }
+
+  private void handleScale(ScaleGestureDetector detector) {
+    // The focus values are only meaningful when the motion is in progress
+    if (detector.isInProgress()) {
+      float focusX = detector.getFocusX();
+      float focusY = detector.getFocusY();
+      float scaleFactor = detector.getScaleFactor();
+      mTemplateContext.getSurfaceCallbackHandler().onScale(focusX, focusY, scaleFactor);
+
+      if (mTemplateContext.getDebugOverlayHandler().isActive()) {
+        updateDebugOverlay(
+            "scale focus [X: "
+                + mDecimalFormat.format(focusX)
+                + ", Y: "
+                + mDecimalFormat.format(focusY)
+                + "], factor ["
+                + mDecimalFormat.format(scaleFactor)
+                + "]");
+      }
+    }
+  }
+
+  private void updateDebugOverlay(String debugText) {
+    if (mDebugOverlayTexts.size() >= MAX_DEBUG_OVERLAY_LINES) {
+      mDebugOverlayTexts.removeFirst();
+    }
+    mDebugOverlayTexts.addLast(debugText);
+
+    StringBuilder sb = new StringBuilder();
+    for (String text : mDebugOverlayTexts) {
+      sb.append(text);
+      sb.append("\n");
+    }
+
+    // Remove the last newline.
+    sb.setLength(sb.length() - 1);
+    mTemplateContext.getDebugOverlayHandler().updateDebugOverlayEntry("Gesture", sb.toString());
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java
new file mode 100644
index 0000000..17ef88e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.Place;
+import androidx.lifecycle.LifecycleRegistry;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Represents a layout that wraps a map view. */
+public interface MapViewContainer {
+  /**
+   * Returns the {@link LifecycleRegistry} instance that can be used by a parent of the container to
+   * drive the lifecycle events of the map view wrapped by it.
+   */
+  @NonNull
+  LifecycleRegistry getLifecycleRegistry();
+
+  /**
+   * Sets whether current location is enabled.
+   *
+   * @param enable true if the map should show the current location
+   */
+  void setCurrentLocationEnabled(boolean enable);
+
+  /** Sets the map anchor. The camera will be adjusted to include the anchor marker if necessary. */
+  void setAnchor(@Nullable Place anchor);
+
+  /**
+   * Sets the places to display in the map. The camera will be moved to the region that contains all
+   * the places.
+   */
+  void setPlaces(List<Place> places);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java
new file mode 100644
index 0000000..dbce0f8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.os.RemoteException;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/**
+ * A {@link AppServiceCall} decorated with a name, useful for logging.
+ *
+ * @param <ServiceT> the type of service to make the call for.
+ */
+public class NamedAppServiceCall<ServiceT> implements AppServiceCall<ServiceT> {
+  private final AppServiceCall<ServiceT> mCall;
+  private final CarAppApi mCarAppApi;
+
+  /** Creates an instance of a {@link NamedAppServiceCall} for the given API. */
+  public static <ServiceT> NamedAppServiceCall<ServiceT> create(
+      CarAppApi carAppApi, AppServiceCall<ServiceT> call) {
+    return new NamedAppServiceCall<>(carAppApi, call);
+  }
+
+  /** Returns the API this call is made for. */
+  public CarAppApi getCarAppApi() {
+    return mCarAppApi;
+  }
+
+  @Override
+  public void dispatch(ServiceT appService, ANRHandler.ANRToken anrToken) throws RemoteException {
+    mCall.dispatch(appService, anrToken);
+  }
+
+  @Override
+  public String toString() {
+    return "[" + mCarAppApi.name() + "]";
+  }
+
+  private NamedAppServiceCall(CarAppApi carAppApi, AppServiceCall<ServiceT> call) {
+    mCall = call;
+    mCarAppApi = carAppApi;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java
new file mode 100644
index 0000000..a7084d0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import static com.android.car.libraries.apphost.logging.TelemetryHandler.getErrorType;
+
+import android.content.ComponentName;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.car.app.FailureResponse;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+
+/**
+ * Default {@link IOnDoneCallback} that will log telemetry for API success and failure, handle ANR,
+ * as well as release the blocking thread, by setting a {@code null} on the blocking response for
+ * any api that blocks for this callback.
+ */
+public class OnDoneCallbackStub extends IOnDoneCallback.Stub implements OnDoneCallback {
+  private final ErrorHandler mErrorHandler;
+  private final ComponentName mAppName;
+  private final ANRHandler.ANRToken mANRToken;
+  private final TelemetryHandler mTelemetryHandler;
+  private final AppBindingStateProvider mAppBindingStateProvider;
+
+  /**
+   * Constructs an {@link OnDoneCallbackStub} that will release the given {@link
+   * ANRHandler.ANRToken} when {@link #onSuccess} or {@link #onFailure} is called.
+   */
+  public OnDoneCallbackStub(TemplateContext templateContext, ANRHandler.ANRToken anrToken) {
+    this(
+        templateContext.getErrorHandler(),
+        templateContext.getCarAppPackageInfo().getComponentName(),
+        anrToken,
+        templateContext.getTelemetryHandler(),
+        templateContext.getAppBindingStateProvider());
+  }
+
+  /**
+   * Constructs an {@link OnDoneCallbackStub} that will release the given {@link
+   * ANRHandler.ANRToken} when {@link #onSuccess} or {@link #onFailure} is called.
+   */
+  public OnDoneCallbackStub(
+      ErrorHandler errorHandler,
+      ComponentName appName,
+      ANRHandler.ANRToken anrToken,
+      TelemetryHandler telemetryHandler,
+      AppBindingStateProvider appBindingStateProvider) {
+    mErrorHandler = errorHandler;
+    mAppName = appName;
+    mANRToken = anrToken;
+    mTelemetryHandler = telemetryHandler;
+    mAppBindingStateProvider = appBindingStateProvider;
+  }
+
+  @CallSuper
+  @Override
+  public void onSuccess(@Nullable Bundleable response) {
+    mANRToken.dismiss();
+    mTelemetryHandler.logCarAppApiSuccessTelemetry(mAppName, mANRToken.getCarAppApi());
+  }
+
+  @CallSuper
+  @Override
+  public void onFailure(Bundleable failureResponse) {
+    mANRToken.dismiss();
+    ThreadUtils.runOnMain(
+        () -> {
+          FailureResponse failure;
+          try {
+            failure = (FailureResponse) failureResponse.get();
+
+            CarAppError.Builder errorBuilder =
+                CarAppError.builder(mAppName).setDebugMessage(failure.getStackTrace());
+            if (shouldLogTelemetryForError(
+                mANRToken.getCarAppApi(), mAppBindingStateProvider.isAppBound())) {
+              mTelemetryHandler.logCarAppApiFailureTelemetry(
+                  mAppName, mANRToken.getCarAppApi(), getErrorType(failure));
+            } else {
+              errorBuilder.setLogVerbose(true);
+            }
+
+            mErrorHandler.showError(errorBuilder.build());
+          } catch (BundlerException e) {
+            mErrorHandler.showError(CarAppError.builder(mAppName).setCause(e).build());
+
+            // If we fail to unbundle the response, log telemetry as a failed IPC due to bundling.
+            mTelemetryHandler.logCarAppApiFailureTelemetry(
+                mAppName, mANRToken.getCarAppApi(), getErrorType(new FailureResponse(e)));
+          }
+        });
+  }
+
+  private static boolean shouldLogTelemetryForError(CarAppApi api, boolean isAppBound) {
+    boolean isApiPreBinding;
+    switch (api) {
+      case GET_APP_VERSION:
+      case ON_HANDSHAKE_COMPLETED:
+      case ON_APP_CREATE:
+        isApiPreBinding = true;
+        break;
+      default:
+        isApiPreBinding = false;
+    }
+    return isAppBound || isApiPreBinding;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java
new file mode 100644
index 0000000..2e9aeea
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.os.RemoteException;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+
+/**
+ * A request to send over the wire to the app.
+ *
+ * <p>The method interface of the client should be marked {@code oneway}.
+ *
+ * <p>You should not call {@link #send} yourself, but rather use the {@link AppDispatcher} to send
+ * this request. This allows for a single location to handle exceptions and performing IPC.
+ */
+public interface OneWayIPC {
+  /** Sends an IPC to the app, using the given {@link ANRToken}. */
+  void send(ANRToken anrToken) throws BundlerException, RemoteException;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java
new file mode 100644
index 0000000..72b5adc
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/**
+ * Manages the state of routing information in template apps.
+ *
+ * <p>This class tracks the state of routing information across multiple template apps.
+ */
+public interface RoutingInfoState {
+  /** Sets whether routing information is visible on the car screen. */
+  void setIsRoutingInfoVisible(boolean isVisible);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java
new file mode 100644
index 0000000..4104d33
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java
@@ -0,0 +1,555 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * This class is forked from {@link android.view.ScaleGestureDetector} in order to modify {@link
+ * #mMinSpan} attribute.
+ *
+ * <p>{@link #mMinSpan} caused the detector to ignore pinch-zoom events when the distance between
+ * the fingers was too small. See b/193927730 for more details.
+ */
+public class ScaleGestureDetector {
+  private static final String TAG = "ScaleGestureDetector";
+
+  /**
+   * The listener for receiving notifications when gestures occur. If you want to listen for all the
+   * different gestures then implement this interface. If you only want to listen for a subset it
+   * might be easier to extend {@link SimpleOnScaleGestureListener}.
+   *
+   * <p>An application will receive events in the following order:
+   *
+   * <ul>
+   *   <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
+   *   <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
+   *   <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
+   * </ul>
+   */
+  public interface OnScaleGestureListener {
+    /**
+     * Responds to scaling events for a gesture in progress. Reported by pointer motion.
+     *
+     * @param detector The detector reporting the event - use this to retrieve extended info about
+     *     event state.
+     * @return Whether or not the detector should consider this event as handled. If an event was
+     *     not handled, the detector will continue to accumulate movement until an event is handled.
+     *     This can be useful if an application, for example, only wants to update scaling factors
+     *     if the change is greater than 0.01.
+     */
+    boolean onScale(ScaleGestureDetector detector);
+
+    /**
+     * Responds to the beginning of a scaling gesture. Reported by new pointers going down.
+     *
+     * @param detector The detector reporting the event - use this to retrieve extended info about
+     *     event state.
+     * @return Whether or not the detector should continue recognizing this gesture. For example, if
+     *     a gesture is beginning with a focal point outside of a region where it makes sense,
+     *     onScaleBegin() may return false to ignore the rest of the gesture.
+     */
+    boolean onScaleBegin(ScaleGestureDetector detector);
+
+    /**
+     * Responds to the end of a scale gesture. Reported by existing pointers going up.
+     *
+     * <p>Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} and {@link
+     * ScaleGestureDetector#getFocusY()} will return focal point of the pointers remaining on the
+     * screen.
+     *
+     * @param detector The detector reporting the event - use this to retrieve extended info about
+     *     event state.
+     */
+    void onScaleEnd(ScaleGestureDetector detector);
+  }
+
+  /**
+   * A convenience class to extend when you only want to listen for a subset of scaling-related
+   * events. This implements all methods in {@link OnScaleGestureListener} but does nothing. {@link
+   * OnScaleGestureListener#onScale(ScaleGestureDetector)} returns {@code false} so that a subclass
+   * can retrieve the accumulated scale factor in an overridden onScaleEnd. {@link
+   * OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns {@code true}.
+   */
+  public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+      return false;
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+      return true;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+      // Intentionally empty
+    }
+  }
+
+  private final Context mContext;
+  private final OnScaleGestureListener mListener;
+
+  private float mFocusX;
+  private float mFocusY;
+
+  private boolean mQuickScaleEnabled;
+  private boolean mStylusScaleEnabled;
+
+  private float mCurrSpan;
+  private float mPrevSpan;
+  private float mInitialSpan;
+  private float mCurrSpanX;
+  private float mCurrSpanY;
+  private float mPrevSpanX;
+  private float mPrevSpanY;
+  private long mCurrTime;
+  private long mPrevTime;
+  private boolean mInProgress;
+  private final int mSpanSlop;
+  private final int mMinSpan;
+
+  private final Handler mHandler;
+
+  private float mAnchoredScaleStartX;
+  private float mAnchoredScaleStartY;
+  private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+
+  private static final float SCALE_FACTOR = .5f;
+  private static final int ANCHORED_SCALE_MODE_NONE = 0;
+  private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
+  private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
+
+  private GestureDetector mGestureDetector;
+
+  private boolean mEventBeforeOrAboveStartingGestureEvent;
+
+  /**
+   * Creates a ScaleGestureDetector with the supplied listener. You may only use this constructor
+   * from a {@link android.os.Looper Looper} thread.
+   *
+   * @param context the application's context
+   * @param listener the listener invoked for all the callbacks, this must not be null.
+   * @throws NullPointerException if {@code listener} is null.
+   */
+  @SuppressWarnings("nullness:argument")
+  public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
+    this(context, listener, null, -1);
+  }
+
+  /**
+   * Creates a ScaleGestureDetector with the supplied listener.
+   *
+   * @see android.os.Handler#Handler()
+   * @param context the application's context
+   * @param listener the listener invoked for all the callbacks, this must not be null.
+   * @param handler the handler to use for running deferred listener events.
+   * @param minSpan the minimum span for the gesture to be recognized as a scale event.
+   * @throws NullPointerException if {@code listener} is null.
+   */
+  @SuppressWarnings("nullness:method.invocation")
+  public ScaleGestureDetector(
+      Context context, OnScaleGestureListener listener, Handler handler, int minSpan) {
+    mContext = context;
+    mListener = listener;
+    final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
+    mSpanSlop = viewConfiguration.getScaledTouchSlop() * 2;
+    mMinSpan = Math.max(minSpan, 0);
+    mHandler = handler;
+    // Quick scale is enabled by default after JB_MR2
+    final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+    if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
+      setQuickScaleEnabled(true);
+    }
+    // Stylus scale is enabled by default after LOLLIPOP_MR1
+    if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
+      setStylusScaleEnabled(true);
+    }
+  }
+
+  /**
+   * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} when
+   * appropriate.
+   *
+   * <p>Applications should pass a complete and consistent event stream to this method. A complete
+   * and consistent event stream involves all MotionEvents from the initial ACTION_DOWN to the final
+   * ACTION_UP or ACTION_CANCEL.
+   *
+   * @param event The event to process
+   * @return true if the event was processed and the detector wants to receive the rest of the
+   *     MotionEvents in this event stream.
+   */
+  public boolean onTouchEvent(MotionEvent event) {
+    mCurrTime = event.getEventTime();
+
+    final int action = event.getActionMasked();
+
+    // Forward the event to check for double tap gesture
+    if (mQuickScaleEnabled) {
+      mGestureDetector.onTouchEvent(event);
+    }
+
+    final int count = event.getPointerCount();
+    final boolean isStylusButtonDown =
+        (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
+
+    final boolean anchoredScaleCancelled =
+        mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
+    final boolean streamComplete =
+        action == MotionEvent.ACTION_UP
+            || action == MotionEvent.ACTION_CANCEL
+            || anchoredScaleCancelled;
+
+    if (action == MotionEvent.ACTION_DOWN || streamComplete) {
+      // Reset any scale in progress with the listener.
+      // If it's an ACTION_DOWN we're beginning a new event stream.
+      // This means the app probably didn't give us all the events. Shame on it.
+      if (mInProgress) {
+        mListener.onScaleEnd(this);
+        mInProgress = false;
+        mInitialSpan = 0;
+        mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+      } else if (inAnchoredScaleMode() && streamComplete) {
+        mInProgress = false;
+        mInitialSpan = 0;
+        mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
+      }
+
+      if (streamComplete) {
+        return true;
+      }
+    }
+
+    if (!mInProgress
+        && mStylusScaleEnabled
+        && !inAnchoredScaleMode()
+        && !streamComplete
+        && isStylusButtonDown) {
+      // Start of a button scale gesture
+      mAnchoredScaleStartX = event.getX();
+      mAnchoredScaleStartY = event.getY();
+      mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
+      mInitialSpan = 0;
+    }
+
+    final boolean configChanged =
+        action == MotionEvent.ACTION_DOWN
+            || action == MotionEvent.ACTION_POINTER_UP
+            || action == MotionEvent.ACTION_POINTER_DOWN
+            || anchoredScaleCancelled;
+
+    final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+    final int skipIndex = pointerUp ? event.getActionIndex() : -1;
+
+    // Determine focal point
+    float sumX = 0;
+    float sumY = 0;
+    final int div = pointerUp ? count - 1 : count;
+    final float focusX;
+    final float focusY;
+    if (inAnchoredScaleMode()) {
+      // In anchored scale mode, the focal pt is always where the double tap
+      // or button down gesture started
+      focusX = mAnchoredScaleStartX;
+      focusY = mAnchoredScaleStartY;
+      if (event.getY() < focusY) {
+        mEventBeforeOrAboveStartingGestureEvent = true;
+      } else {
+        mEventBeforeOrAboveStartingGestureEvent = false;
+      }
+    } else {
+      for (int i = 0; i < count; i++) {
+        if (skipIndex == i) {
+          continue;
+        }
+        sumX += event.getX(i);
+        sumY += event.getY(i);
+      }
+
+      focusX = sumX / div;
+      focusY = sumY / div;
+    }
+
+    // Determine average deviation from focal point
+    float devSumX = 0;
+    float devSumY = 0;
+    for (int i = 0; i < count; i++) {
+      if (skipIndex == i) {
+        continue;
+      }
+
+      // Convert the resulting diameter into a radius.
+      devSumX += Math.abs(event.getX(i) - focusX);
+      devSumY += Math.abs(event.getY(i) - focusY);
+    }
+    final float devX = devSumX / div;
+    final float devY = devSumY / div;
+
+    // Span is the average distance between touch points through the focal point;
+    // i.e. the diameter of the circle with a radius of the average deviation from
+    // the focal point.
+    final float spanX = devX * 2;
+    final float spanY = devY * 2;
+    final float span;
+    if (inAnchoredScaleMode()) {
+      span = spanY;
+    } else {
+      span = (float) Math.hypot(spanX, spanY);
+    }
+
+    // Dispatch begin/end events as needed.
+    // If the configuration changes, notify the app to reset its current state by beginning
+    // a fresh scale event stream.
+    final boolean wasInProgress = mInProgress;
+    mFocusX = focusX;
+    mFocusY = focusY;
+    if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
+      mListener.onScaleEnd(this);
+      mInProgress = false;
+      mInitialSpan = span;
+    }
+    if (configChanged) {
+      mPrevSpanX = mCurrSpanX = spanX;
+      mPrevSpanY = mCurrSpanY = spanY;
+      mInitialSpan = mPrevSpan = mCurrSpan = span;
+    }
+
+    final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
+    if (!mInProgress
+        && span >= minSpan
+        && (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
+      mPrevSpanX = mCurrSpanX = spanX;
+      mPrevSpanY = mCurrSpanY = spanY;
+      mPrevSpan = mCurrSpan = span;
+      mPrevTime = mCurrTime;
+      mInProgress = mListener.onScaleBegin(this);
+    }
+
+    // Handle motion; focal point and span/scale factor are changing.
+    if (action == MotionEvent.ACTION_MOVE) {
+      mCurrSpanX = spanX;
+      mCurrSpanY = spanY;
+      mCurrSpan = span;
+
+      boolean updatePrev = true;
+
+      if (mInProgress) {
+        updatePrev = mListener.onScale(this);
+      }
+
+      if (updatePrev) {
+        mPrevSpanX = mCurrSpanX;
+        mPrevSpanY = mCurrSpanY;
+        mPrevSpan = mCurrSpan;
+        mPrevTime = mCurrTime;
+      }
+    }
+
+    return true;
+  }
+
+  private boolean inAnchoredScaleMode() {
+    return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
+  }
+
+  /**
+   * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks when
+   * the user performs a doubleTap followed by a swipe. Note that this is enabled by default if the
+   * app targets API 19 and newer.
+   *
+   * @param scales true to enable quick scaling, false to disable
+   */
+  public void setQuickScaleEnabled(boolean scales) {
+    mQuickScaleEnabled = scales;
+    if (mQuickScaleEnabled && mGestureDetector == null) {
+      GestureDetector.SimpleOnGestureListener gestureListener =
+          new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public boolean onDoubleTap(MotionEvent e) {
+              // Double tap: start watching for a swipe
+              mAnchoredScaleStartX = e.getX();
+              mAnchoredScaleStartY = e.getY();
+              mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
+              return true;
+            }
+          };
+      mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
+    }
+  }
+
+  /**
+   * Return whether the quick scale gesture, in which the user performs a double tap followed by a
+   * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
+   */
+  public boolean isQuickScaleEnabled() {
+    return mQuickScaleEnabled;
+  }
+
+  /**
+   * Sets whether the associates {@link OnScaleGestureListener} should receive onScale callbacks
+   * when the user uses a stylus and presses the button. Note that this is enabled by default if the
+   * app targets API 23 and newer.
+   *
+   * @param scales true to enable stylus scaling, false to disable.
+   */
+  public void setStylusScaleEnabled(boolean scales) {
+    mStylusScaleEnabled = scales;
+  }
+
+  /**
+   * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
+   * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
+   */
+  public boolean isStylusScaleEnabled() {
+    return mStylusScaleEnabled;
+  }
+
+  /** Returns {@code true} if a scale gesture is in progress. */
+  public boolean isInProgress() {
+    return mInProgress;
+  }
+
+  /**
+   * Get the X coordinate of the current gesture's focal point. If a gesture is in progress, the
+   * focal point is between each of the pointers forming the gesture.
+   *
+   * <p>If {@link #isInProgress()} would return false, the result of this function is undefined.
+   *
+   * @return X coordinate of the focal point in pixels.
+   */
+  public float getFocusX() {
+    return mFocusX;
+  }
+
+  /**
+   * Get the Y coordinate of the current gesture's focal point. If a gesture is in progress, the
+   * focal point is between each of the pointers forming the gesture.
+   *
+   * <p>If {@link #isInProgress()} would return false, the result of this function is undefined.
+   *
+   * @return Y coordinate of the focal point in pixels.
+   */
+  public float getFocusY() {
+    return mFocusY;
+  }
+
+  /**
+   * Return the average distance between each of the pointers forming the gesture in progress
+   * through the focal point.
+   *
+   * @return Distance between pointers in pixels.
+   */
+  public float getCurrentSpan() {
+    return mCurrSpan;
+  }
+
+  /**
+   * Return the average X distance between each of the pointers forming the gesture in progress
+   * through the focal point.
+   *
+   * @return Distance between pointers in pixels.
+   */
+  public float getCurrentSpanX() {
+    return mCurrSpanX;
+  }
+
+  /**
+   * Return the average Y distance between each of the pointers forming the gesture in progress
+   * through the focal point.
+   *
+   * @return Distance between pointers in pixels.
+   */
+  public float getCurrentSpanY() {
+    return mCurrSpanY;
+  }
+
+  /**
+   * Return the previous average distance between each of the pointers forming the gesture in
+   * progress through the focal point.
+   *
+   * @return Previous distance between pointers in pixels.
+   */
+  public float getPreviousSpan() {
+    return mPrevSpan;
+  }
+
+  /**
+   * Return the previous average X distance between each of the pointers forming the gesture in
+   * progress through the focal point.
+   *
+   * @return Previous distance between pointers in pixels.
+   */
+  public float getPreviousSpanX() {
+    return mPrevSpanX;
+  }
+
+  /**
+   * Return the previous average Y distance between each of the pointers forming the gesture in
+   * progress through the focal point.
+   *
+   * @return Previous distance between pointers in pixels.
+   */
+  public float getPreviousSpanY() {
+    return mPrevSpanY;
+  }
+
+  /**
+   * Return the scaling factor from the previous scale event to the current event. This value is
+   * defined as ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
+   *
+   * @return The current scaling factor.
+   */
+  public float getScaleFactor() {
+    if (inAnchoredScaleMode()) {
+      // Drag is moving up; the further away from the gesture
+      // start, the smaller the span should be, the closer,
+      // the larger the span, and therefore the larger the scale
+      final boolean scaleUp =
+          mEventBeforeOrAboveStartingGestureEvent
+              ? (mCurrSpan < mPrevSpan)
+              : (mCurrSpan > mPrevSpan);
+      final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
+      return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
+    }
+    return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
+  }
+
+  /**
+   * Return the time difference in milliseconds between the previous accepted scaling event and the
+   * current scaling event.
+   *
+   * @return Time difference since the last scaling event in milliseconds.
+   */
+  public long getTimeDelta() {
+    return mCurrTime - mPrevTime;
+  }
+
+  /**
+   * Return the event time of the current event being processed.
+   *
+   * @return Current event time in milliseconds.
+   */
+  public long getEventTime() {
+    return mCurrTime;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java
new file mode 100644
index 0000000..2f26643
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.view.View;
+
+/**
+ * A manager that allows presenters to control some attributes of the status bar, such as the color
+ * of the text and background.
+ */
+public interface StatusBarManager {
+  /** The type of status bar to display. */
+  enum StatusBarState {
+    /**
+     * The status bar is designed to be rendered over an app drawn surface such as a map, where it
+     * will have a background protection to ensure the user can read the status bar information.
+     */
+    OVER_SURFACE,
+
+    /**
+     * The status bar is designed to be rendered over a dark background (e.g. white text with
+     * transparent background).
+     */
+    LIGHT,
+
+    /** The status bar is designed the status bar */
+    GONE
+  }
+
+  /** Updates the {@link StatusBarState}. */
+  void setStatusBarState(StatusBarState statusBarState, View rootView);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java
new file mode 100644
index 0000000..b1ed7db
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/** Assorted string manipulation utilities. */
+public class StringUtils {
+  /** Milliseconds per unit of time LUT. Needs to be in sync with {@link #UNIT_SUFFIXES}. */
+  private static final long[] MILLIS_PER_UNIT =
+      new long[] {
+        DAYS.toMillis(1),
+        HOURS.toMillis(1),
+        MINUTES.toMillis(1),
+        SECONDS.toMillis(1),
+        1 // 1 millisecond in milliseconds
+      };
+
+  private static final String[] UNIT_SUFFIXES = new String[] {"d", "h", "m", "s", "ms"};
+
+  /**
+   * Returns a compact string representation of a duration.
+   *
+   * <p>The format is {@code "xd:xh:xm:xs:xms"}, where {@code "x"} is an unpadded numeric value. If
+   * {@code "x"} is 0, it is altogether omitted.
+   *
+   * <p>For example, {@code "1d:25m:123ms"} denotes 1 day, 25 minutes, and 123 milliseconds.
+   *
+   * <p>Negative durations are returned as {@code "-"}
+   */
+  public static String formatDuration(long durationMillis) {
+    StringBuilder builder = new StringBuilder();
+    if (durationMillis < 0) {
+      return "-";
+    } else if (durationMillis == 0) {
+      return "0ms";
+    }
+    boolean first = true;
+    for (int i = 0; i < MILLIS_PER_UNIT.length; ++i) {
+      long value =
+          (i > 0 ? (durationMillis % MILLIS_PER_UNIT[i - 1]) : durationMillis) / MILLIS_PER_UNIT[i];
+      if (value > 0) {
+        if (first) {
+          first = false;
+        } else {
+          builder.append(":");
+        }
+        builder.append(value).append(UNIT_SUFFIXES[i]);
+      }
+    }
+    return builder.toString();
+  }
+
+  private StringUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java
new file mode 100644
index 0000000..5ff1e98
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+/** Interface for handling surface callbacks such as pan and zoom. */
+public interface SurfaceCallbackHandler {
+
+  /** Returns whether a new gesture can begin. */
+  default boolean canStartNewGesture() {
+    return true;
+  }
+
+  /**
+   * Forwards a scroll gesture event to the car app's {@link
+   * androidx.car.app.ISurfaceCallback#onScroll(float, float)}.
+   */
+  void onScroll(float distanceX, float distanceY);
+
+  /**
+   * Forwards a fling gesture event to the car app's {@link
+   * androidx.car.app.ISurfaceCallback#onFling(float, float)}.
+   */
+  void onFling(float velocityX, float velocityY);
+
+  /**
+   * Forwards a scale gesture event to the car app's {@link
+   * androidx.car.app.ISurfaceCallback#onScale(float, float, float)}.
+   */
+  void onScale(float focusX, float focusY, float scaleFactor);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java
new file mode 100644
index 0000000..35f9dc3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.graphics.Rect;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A class for storing and retrieving the properties such as the visible area and stable center of a
+ * surface.
+ */
+public interface SurfaceInfoProvider {
+  /**
+   * Returns the {@link Rect} that specifies the region in the view where the templated-content
+   * (e.g. the card container, FAB) currently extends to. Returns {@code null} if the value is not
+   * set.
+   */
+  @Nullable Rect getVisibleArea();
+
+  /**
+   * Sets the safe area and if needed updates the stable center.
+   *
+   * <p>Subscribe to the event {@link EventManager.EventType#SURFACE_VISIBLE_AREA} to be notify when
+   * the safe area has been updated.
+   */
+  void setVisibleArea(Rect safeArea);
+
+  /**
+   * Returns the {@link Rect} that specifies the region of the stable visible area where the
+   * templated content (e.g. card container, action strip) could possibly extend to. It is stable in
+   * that the area is the guaranteed visible no matter any dynamic changes to the view. It is
+   * possible for stable area to increase or decrease due to changes in the template content or a
+   * template change.
+   */
+  @Nullable Rect getStableArea();
+
+  /** Indicates that the stable area should be recalculated the next time the safe area is set. */
+  void invalidateStableArea();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java
new file mode 100644
index 0000000..9217b4e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.common;
+
+import android.os.SystemClock;
+
+/**
+ * Wrapper of SystemClock
+ *
+ * <p>Real instances should just delegate the calls to the static methods, while test instances
+ * return values set manually. See {@link android.os.SystemClock}.
+ */
+public final class SystemClockWrapper {
+  /**
+   * Returns milliseconds since boot, including time spent in sleep.
+   *
+   * @return elapsed milliseconds since boot
+   */
+  public long elapsedRealtime() {
+    return SystemClock.elapsedRealtime();
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java
new file mode 100644
index 0000000..2015581
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.distraction.constraints.ConstraintsProvider;
+import com.android.car.libraries.apphost.input.InputConfig;
+import com.android.car.libraries.apphost.input.InputManager;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Context for various template components to retrieve important bits of information for presenting
+ * content.
+ */
+public abstract class TemplateContext extends ContextWrapper {
+
+  private final Map<Class<? extends AppHostService>, AppHostService> mAppHostServices =
+      new HashMap<>();
+
+  /**
+   * Constructs an instance of a {@link TemplateContext} wrapping the given {@link Context} object.
+   */
+  public TemplateContext(Context base) {
+    super(base);
+  }
+
+  /**
+   * Creates a {@link TemplateContext} that replaces the inner {@link Context} for the given {@link
+   * TemplateContext}.
+   *
+   * <p>This is used for using an uiContext for view elements, since they may have a theme applied
+   * on them.
+   *
+   * @param other the {@link TemplateContext} to wrap for all the getters
+   * @param uiContext the {@link Context} that this instance will wrap
+   */
+  public static TemplateContext from(TemplateContext other, Context uiContext) {
+    return new TemplateContext(uiContext) {
+      @Override
+      public CarAppPackageInfo getCarAppPackageInfo() {
+        return other.getCarAppPackageInfo();
+      }
+
+      @Override
+      public InputManager getInputManager() {
+        return other.getInputManager();
+      }
+
+      @Override
+      public ErrorHandler getErrorHandler() {
+        return other.getErrorHandler();
+      }
+
+      @Override
+      public ANRHandler getAnrHandler() {
+        return other.getAnrHandler();
+      }
+
+      @Override
+      public BackPressedHandler getBackPressedHandler() {
+        return other.getBackPressedHandler();
+      }
+
+      @Override
+      public SurfaceCallbackHandler getSurfaceCallbackHandler() {
+        return other.getSurfaceCallbackHandler();
+      }
+
+      @Override
+      public InputConfig getInputConfig() {
+        return other.getInputConfig();
+      }
+
+      @Override
+      public StatusBarManager getStatusBarManager() {
+        return other.getStatusBarManager();
+      }
+
+      @Override
+      public SurfaceInfoProvider getSurfaceInfoProvider() {
+        return other.getSurfaceInfoProvider();
+      }
+
+      @Override
+      public EventManager getEventManager() {
+        return other.getEventManager();
+      }
+
+      @Override
+      public AppDispatcher getAppDispatcher() {
+        return other.getAppDispatcher();
+      }
+
+      @Override
+      public SystemClockWrapper getSystemClockWrapper() {
+        return other.getSystemClockWrapper();
+      }
+
+      @Override
+      public ToastController getToastController() {
+        return other.getToastController();
+      }
+
+      @Override
+      @Nullable
+      public Context getAppConfigurationContext() {
+        return other.getAppConfigurationContext();
+      }
+
+      @Override
+      public CarAppManager getCarAppManager() {
+        return other.getCarAppManager();
+      }
+
+      @Override
+      public void updateConfiguration(Configuration configuration) {
+        other.updateConfiguration(configuration);
+      }
+
+      @Override
+      public TelemetryHandler getTelemetryHandler() {
+        return other.getTelemetryHandler();
+      }
+
+      @Override
+      public DebugOverlayHandler getDebugOverlayHandler() {
+        return other.getDebugOverlayHandler();
+      }
+
+      @Override
+      public RoutingInfoState getRoutingInfoState() {
+        return other.getRoutingInfoState();
+      }
+
+      @Override
+      public ColorContrastCheckState getColorContrastCheckState() {
+        return other.getColorContrastCheckState();
+      }
+
+      @Override
+      public ConstraintsProvider getConstraintsProvider() {
+        return other.getConstraintsProvider();
+      }
+
+      @Override
+      public CarHostConfig getCarHostConfig() {
+        return other.getCarHostConfig();
+      }
+
+      @Override
+      public HostResourceIds getHostResourceIds() {
+        return other.getHostResourceIds();
+      }
+
+      @Override
+      public AppBindingStateProvider getAppBindingStateProvider() {
+        return other.getAppBindingStateProvider();
+      }
+
+      @Override
+      public boolean registerAppHostService(
+          Class<? extends AppHostService> clazz, AppHostService appHostService) {
+        return other.registerAppHostService(clazz, appHostService);
+      }
+
+      @Override
+      @Nullable
+      public <T extends AppHostService> T getAppHostService(Class<T> clazz) {
+        // TODO(b/169182143): Make single use type services use this getter.
+        return other.getAppHostService(clazz);
+      }
+    };
+  }
+
+  /**
+   * Provides the package information such as accent colors, component names etc. associated with
+   * the 3p app.
+   */
+  public abstract CarAppPackageInfo getCarAppPackageInfo();
+
+  /** Provides the {@link InputManager} for the current car activity to bring up the keyboard. */
+  public abstract InputManager getInputManager();
+
+  /** Provides the {@link ErrorHandler} for displaying errors to the user. */
+  public abstract ErrorHandler getErrorHandler();
+
+  /** Provides the {@link ANRHandler} for managing ANRs. */
+  public abstract ANRHandler getAnrHandler();
+
+  /** Provides the {@link BackPressedHandler} for dispatching back press events to the app. */
+  public abstract BackPressedHandler getBackPressedHandler();
+
+  /** Provides the {@link SurfaceCallbackHandler} for dispatching surface callbacks to the app. */
+  public abstract SurfaceCallbackHandler getSurfaceCallbackHandler();
+
+  /** Provides the {@link InputConfig} to access the input configuration. */
+  public abstract InputConfig getInputConfig();
+
+  /**
+   * Provides the {@link StatusBarManager} to allow for overriding the status bar background and
+   * text color.
+   */
+  public abstract StatusBarManager getStatusBarManager();
+
+  /** Provides the {@link SurfaceInfoProvider} to allow storing and retrieving safe area insets. */
+  public abstract SurfaceInfoProvider getSurfaceInfoProvider();
+
+  /** Provides the {@link EventManager} to allow dispatching and subscribing to different events. */
+  public abstract EventManager getEventManager();
+
+  /** Provides the {@link AppDispatcher} which allows dispatching IPCs to the client app. */
+  public abstract AppDispatcher getAppDispatcher();
+
+  /** Returns the system {@link SystemClockWrapper}. */
+  public abstract SystemClockWrapper getSystemClockWrapper();
+
+  /** Returns the {@link ToastController} which allows clients to show toasts. */
+  public abstract ToastController getToastController();
+
+  /**
+   * Returns a {@link Context} instance for the remote app, configured with this context's
+   * configuration (which includes configuration from the car's resources, such as screen size and
+   * DPI).
+   *
+   * <p>The theme in this context is also set to the application's theme id, so that attributes in
+   * remote resources can be resolved using the that theme (see {@link
+   * ColorUtils#loadThemeId(Context, ComponentName)}).
+   *
+   * <p>Use method to load drawable resources from app's APKs, so that they are returned with the
+   * target DPI of the car screen, rather than the phone's. See b/159088813 for more details.
+   *
+   * @return the remote app's context, or {@code null} if unavailable due to an error (the logcat
+   *     will contain a log with the error in this case).
+   */
+  @Nullable
+  public abstract Context getAppConfigurationContext();
+
+  /** Returns the {@link CarAppManager} that is to be used for starting and finishing car apps. */
+  public abstract CarAppManager getCarAppManager();
+
+  /**
+   * Updates the {@link Configuration} of the app configuration context that is retrieved via {@link
+   * #getAppConfigurationContext}, and publishes a {@link
+   * EventManager.EventType#CONFIGURATION_CHANGED} event using the {@link EventManager}.
+   */
+  public abstract void updateConfiguration(Configuration configuration);
+
+  /** Returns the {@link TelemetryHandler} instance that allows reporting telemetry data. */
+  public abstract TelemetryHandler getTelemetryHandler();
+
+  /** Returns the {@link DebugOverlayHandler} instance that updating the debug overlay. */
+  public abstract DebugOverlayHandler getDebugOverlayHandler();
+
+  /**
+   * Returns the {@link RoutingInfoState} that keeps track of the routing information state across
+   * template apps.
+   */
+  // TODO(b/169182143): Use a generic getService model to retrieve this object
+  public abstract RoutingInfoState getRoutingInfoState();
+
+  /**
+   * Returns the {@link RoutingInfoState} that keeps track of the color contrast check state in the
+   * current template.
+   */
+  public abstract ColorContrastCheckState getColorContrastCheckState();
+
+  /**
+   * Returns the {@link ConstraintsProvider} that can provide the limits associated with this car
+   * app.
+   */
+  public abstract ConstraintsProvider getConstraintsProvider();
+
+  /**
+   * Returns a {@link CarHostConfig} object containing a series of flags and configuration options
+   */
+  public abstract CarHostConfig getCarHostConfig();
+
+  /** Produces a status report for this context, used for diagnostics and logging. */
+  public void reportStatus(PrintWriter pw) {}
+
+  /** Returns the {@link HostResourceIds} to use for this host implementation */
+  public abstract HostResourceIds getHostResourceIds();
+
+  /** Returns the {@link AppBindingStateProvider} instance. */
+  public abstract AppBindingStateProvider getAppBindingStateProvider();
+
+  /**
+   * Dynamically registers a {@link AppHostService}.
+   *
+   * @return {@code true} if register is successful, {@code false} if the service already exists.
+   */
+  public boolean registerAppHostService(
+      Class<? extends AppHostService> clazz, AppHostService appHostService) {
+    if (mAppHostServices.containsKey(clazz)) {
+      return false;
+    }
+
+    mAppHostServices.put(clazz, appHostService);
+    return true;
+  }
+
+  /**
+   * Returns the {@link AppHostService} of the requested class, or {@code null} if it does not exist
+   * for this host.
+   */
+  @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof T
+  @Nullable
+  public <T extends AppHostService> T getAppHostService(Class<T> clazz) {
+    return (T) mAppHostServices.get(clazz);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java
new file mode 100644
index 0000000..25c0985
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/** Utility functions to handle running functions on the main thread. */
+public class ThreadUtils {
+  private static final Handler HANDLER = new Handler(Looper.getMainLooper());
+
+  /** Field assignment is atomic in java and we are only checking reference equality. */
+  private static Thread sMainThread;
+
+  /** Executes the {@code action} on the main thread. */
+  public static void runOnMain(Runnable action) {
+    if (Looper.getMainLooper() == Looper.myLooper()) {
+      action.run();
+    } else {
+      HANDLER.post(action);
+    }
+  }
+
+  /** Enqueues the {@code action} to the message queue on the main thread. */
+  public static void enqueueOnMain(Runnable action) {
+    HANDLER.post(action);
+  }
+
+  /**
+   * Checks that currently running on the main thread.
+   *
+   * @throws IllegalStateException if the current thread is not the main thread
+   */
+  public static void checkMainThread() {
+    if (Looper.getMainLooper() != Looper.myLooper()) {
+      throw new IllegalStateException("Not running on main thread when it is required to.");
+    }
+  }
+
+  /** Returns true if the current thread is the UI thread. */
+  public static boolean getsMainThread() {
+    if (sMainThread == null) {
+      sMainThread = Looper.getMainLooper().getThread();
+    }
+    return Thread.currentThread() == sMainThread;
+  }
+
+  /** Checks that the current thread is the UI thread. Otherwise throws an exception. */
+  public static void ensureMainThread() {
+    if (!getsMainThread()) {
+      throw new AssertionError("Must be called on the UI thread");
+    }
+  }
+
+  private ThreadUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java
new file mode 100644
index 0000000..a1ebafe
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.common;
+
+import android.widget.Toast;
+
+/** Allows controlling the toasts on car screen. */
+public interface ToastController {
+  /**
+   * Shows the Toast view with the specified text for the specified duration.
+   *
+   * @param text the text message to be displayed
+   * @param duration how long to display the message. Either {@link Toast#LENGTH_SHORT} or {@link
+   *     Toast#LENGTH_LONG}
+   */
+  void showToast(CharSequence text, int duration);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java
new file mode 100644
index 0000000..242591a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/BackFlowViolationException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction;
+
+/** A {@link FlowViolationException} that indicates an incorrect back flow. */
+public class BackFlowViolationException extends FlowViolationException {
+  BackFlowViolationException(String message) {
+    super(message);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java
new file mode 100644
index 0000000..c0685d0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/FlowViolationException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction;
+
+/** Wrapper class for exceptions that indicate template flow violations. */
+public abstract class FlowViolationException extends Exception {
+  protected FlowViolationException(String message) {
+    super(message);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java
new file mode 100644
index 0000000..4892bf7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/OverLimitFlowViolationException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction;
+
+/** A {@link FlowViolationException} that indicates the flow is over the max limit. */
+public class OverLimitFlowViolationException extends FlowViolationException {
+  protected OverLimitFlowViolationException(String message) {
+    super(message);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java
new file mode 100644
index 0000000..04d5118
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/TemplateValidator.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction;
+
+import android.content.Context;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateInfo;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.signin.SignInTemplate;
+import androidx.car.app.navigation.model.NavigationTemplate;
+import com.android.car.libraries.apphost.common.AppHostService;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.distraction.checkers.TemplateChecker;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A class for validating whether an app's template flow abide by the flow rules.
+ *
+ * <p>The host should call {@link #validateFlow} to check whether a new template is allowed in the
+ * flow, governed by the following rules:
+ *
+ * <ul>
+ *   <li>BACK: if the new template contains the same ID and type as another template that have
+ *       already been seen, it is considered a back operation, and the step count will be reset to
+ *       the value used for the previously-seen template.
+ *   <li>REFRESH: if the new template does not contain different immutable contents compared to the
+ *       most recent template, it is considered a refresh and the step count will not increased.
+ *   <li>NEW: Otherwise, the template is considered a new view and is only allowed if the given step
+ *       limit has not been reached. If the template is allowed and is a consumption view, as
+ *       defined by {@link #isConsumptionView}, the step count is reset and the next new template
+ *       will start from a step count of zero again.
+ * </ul>
+ *
+ * <p>See go/watevra-distraction-part1 for more details.
+ */
+public class TemplateValidator implements AppHostService {
+  private final int mStepLimit;
+  private @Nullable TemplateWrapper mLastTemplateWrapper;
+  private final Deque<TemplateStackItem> mTemplateItemStack = new ArrayDeque<>();
+
+  /**
+   * When set to the true, the next template received for validation will have its step reset tot
+   * zero (e.g. the template will be considered the start of a new task).
+   */
+  private boolean mIsReset;
+
+  /**
+   * When set to the true, the next template received for validation will be considered a refresh
+   * regardless of content as long as it is of the same type.
+   */
+  private boolean mIsNextTemplateContentRefreshIfSameType;
+
+  /**
+   * The step count of the last sent template.
+   *
+   * <p>Note that this value is 1-based. For example, the first template is step 1.
+   */
+  private int mLastStep;
+
+  private final Map<Class<? extends Template>, TemplateChecker<? extends Template>>
+      mTemplateCheckerMap = new HashMap<>();
+
+  /** Constructs a {@link TemplateValidator} instance with a given maximum number of steps. */
+  public static TemplateValidator create(int stepLimit) {
+    return new TemplateValidator(stepLimit);
+  }
+
+  /**
+   * Registers a {@link TemplateChecker} to be used for the template type during the {@link
+   * #validateFlow} operation.
+   */
+  public <T extends Template> void registerTemplateChecker(
+      Class<T> templateClass, TemplateChecker<T> checker) {
+    mTemplateCheckerMap.put(templateClass, checker);
+  }
+
+  /** Reset the current step count on the next template received. */
+  public void reset() {
+    // Note that we don't clear the stack here. The host needs to keep track of the templates
+    // it has seen, so that it can compare the list of TemplateInfo inside TemplateWrapper,
+    // and not count them after the refresh. See b/179085934 for more details.
+    // Additionally, we don't reset mLastTemplateWrapper since that will mean navigating out of
+    // the app (IE to the launcher) and back will cause the template to be recreated rather than
+    // refreshed.
+    mIsReset = true;
+  }
+
+  /**
+   * Sets whether the next template should be considered a refresh as long as it is of the same
+   * type.
+   */
+  public void setIsNextTemplateContentRefreshIfSameType(boolean isContentRefresh) {
+    mIsNextTemplateContentRefreshIfSameType = isContentRefresh;
+  }
+
+  /** Whether the next template should be considered a refresh as long as it is of the same type. */
+  @VisibleForTesting
+  boolean isNextTemplateContentRefreshIfSameType() {
+    return mIsNextTemplateContentRefreshIfSameType;
+  }
+
+  /** Returns the step count that was used for the last template. */
+  @VisibleForTesting
+  public int getLastStep() {
+    return mLastStep;
+  }
+
+  /** Returns whether the validator will reset the step count on the next template received. */
+  @VisibleForTesting
+  public boolean isPendingReset() {
+    return mIsReset;
+  }
+
+  @Override
+  public String toString() {
+    return "[ step limit: " + mStepLimit + ", last step: " + mLastStep + "]";
+  }
+
+  /**
+   * Validates whether the application has the required permissions for this template.
+   *
+   * @throws SecurityException if the application is missing any required permission
+   */
+  @SuppressWarnings({"rawtypes", "unchecked"}) // ignoring TemplateChecker raw type warnings.
+  public void validateHasRequiredPermissions(
+      TemplateContext templateContext, TemplateWrapper templateWrapper) {
+    Template template = templateWrapper.getTemplate();
+
+    TemplateChecker checker = mTemplateCheckerMap.get(template.getClass());
+    if (checker == null) {
+      throw new IllegalStateException(
+          "Permission check failed. No checker has been registered for the template"
+              + " type: "
+              + template);
+    }
+
+    Context appConfigurationContext = templateContext.getAppConfigurationContext();
+    if (appConfigurationContext == null) {
+      L.d(
+          LogTags.DISTRACTION,
+          "Permission check failed. No app configuration context is registered.");
+      // If we do not have a context for the car app do not allow due to missing
+      // permissions, this is a bad state.
+      throw new IllegalStateException(
+          "Could not validate whether the app has required permissions");
+    }
+
+    checker.checkPermissions(appConfigurationContext, template);
+  }
+
+  /**
+   * Validates whether the given {@link TemplateWrapper} meets the flow restriction requirements.
+   *
+   * @throws FlowViolationException if the new template contains the same ID as a previously seen
+   *     template but is of a different template type
+   * @throws FlowViolationException if the step limit has been reached and the template is not
+   */
+  public void validateFlow(TemplateWrapper templateWrapper) throws FlowViolationException {
+    fillInBackStackIfNeeded(templateWrapper);
+
+    boolean isNextTemplateContentRefreshIfSameType = mIsNextTemplateContentRefreshIfSameType;
+    mIsNextTemplateContentRefreshIfSameType = false;
+
+    // Order is important here. We want to make sure we check for back first because there
+    // might be cases when an app goes back from one template to another, the content changes
+    // might satisfy the refresh conditions, thus keeping the current step instead of
+    // decrementing to the previous step.
+    if (validateBackFlow(templateWrapper)
+        || validateRefreshFlow(templateWrapper, isNextTemplateContentRefreshIfSameType)) {
+      mLastTemplateWrapper = templateWrapper;
+      return;
+    }
+
+    // Before we check whether a new template is allowed, check whether a reset should happen so
+    // we don't prematurely disallow the next template.
+    Template template = templateWrapper.getTemplate();
+
+    // Parked-only template should not increment step count.
+    int currentStep = isParkedOnlyTemplate(template.getClass()) ? mLastStep : mLastStep + 1;
+    currentStep = resetTaskStepIfNeeded(currentStep, template.getClass());
+
+    throwIfNewTemplateDisallowed(currentStep, templateWrapper);
+
+    L.d(
+        LogTags.DISTRACTION,
+        "NEW template detected. Task step currently at %d of %d. %s",
+        currentStep,
+        mStepLimit,
+        templateWrapper);
+
+    templateWrapper.setCurrentTaskStep(currentStep);
+    mTemplateItemStack.push(
+        new TemplateStackItem(
+            templateWrapper.getId(), template.getClass(), templateWrapper.getCurrentTaskStep()));
+    mLastTemplateWrapper = templateWrapper;
+    mLastStep = currentStep;
+  }
+
+  private void fillInBackStackIfNeeded(TemplateWrapper templateWrapper) {
+    // The template infos are ordered as follows:  top, second, third, bottom
+    // Look through our known stack, if there are more than 1 templates in the top that we do
+    // not currently have, we need to add them to our stack.
+    //
+    // If there is 1 extra template, it'll be handled by the pushing logic in validateFlow.
+    //
+    // If there the top template ids are the same, it will be handled by the logic in
+    // validateRefreshFlow.
+    //
+    // If there are less in the client provided stack, it will be handled by the logic in
+    // validateBackFlow.
+    Deque<TemplateInfo> newTemplates = new ArrayDeque<>();
+    for (TemplateInfo templateInfo : templateWrapper.getTemplateInfosForScreenStack()) {
+      // For each not known template push it onto a separate stack, so that after all the
+      // pushes, it will be ordered as follows:
+      //
+      // i.e. if the client has 3 new templates that the host does not know about this
+      // temporary stack will be third, second, top
+      if (findExistingTemplateStackItem(templateInfo.getTemplateId()) == null) {
+        newTemplates.push(templateInfo);
+      } else {
+        break;
+      }
+    }
+
+    // At this point the "newTemplates" stack contains any values they are new templates that
+    // the host does not know about.
+    // We do not need to push the bottom of this new stack, as that is the new template which
+    // will be handled by validateFlow.
+    while (newTemplates.size() > 1) {
+      // Set last template wrapper to null so that we don't check if the new one is possibly a
+      // refresh since we are preseeding templates in between the current top and the new top.
+      mLastTemplateWrapper = null;
+      TemplateInfo info = newTemplates.pop();
+      Class<? extends Template> templateClass = info.getTemplateClass();
+
+      // Parked-only template should not increment step count.
+      int currentStep = isParkedOnlyTemplate(templateClass) ? mLastStep : mLastStep + 1;
+      currentStep = resetTaskStepIfNeeded(currentStep, templateClass);
+      mTemplateItemStack.push(
+          new TemplateStackItem(info.getTemplateId(), templateClass, currentStep));
+      mLastStep = currentStep;
+    }
+  }
+
+  /**
+   * Returns {@code true} if the given {@link TemplateWrapper} is a refresh of the last-sent
+   * template based on the registered {@link TemplateChecker}, or {@code false otherwise}.
+   *
+   * <p>Note that if a {@link TemplateChecker} is not available for a template type, the {@link
+   * #validateFlow} operation will return false by default.
+   *
+   * <p>A template is considered a refresh if it is of the same template type and does not have data
+   * that we consider immutable as compared to the previous template. If the input template is
+   * deemed a refresh, the task step count will be changed.
+   */
+  @SuppressWarnings({"rawtypes", "unchecked"}) // ignoring TemplateChecker raw type warnings.
+  private boolean validateRefreshFlow(
+      TemplateWrapper templateWrapper, boolean isNextTemplateContentRefreshIfSameType) {
+    TemplateWrapper lastTemplateWrapper = mLastTemplateWrapper;
+    TemplateStackItem lastTemplateStackItem = mTemplateItemStack.peek();
+    if (lastTemplateWrapper == null || lastTemplateStackItem == null) {
+      return false;
+    }
+
+    Template lastTemplate = lastTemplateWrapper.getTemplate();
+    Template newTemplate = templateWrapper.getTemplate();
+
+    TemplateChecker checker = mTemplateCheckerMap.get(newTemplate.getClass());
+    if (checker == null) {
+      L.d(
+          LogTags.DISTRACTION,
+          "REFRESH check failed. No checker has been registered for the template type:" + " %s",
+          newTemplate.getClass());
+      return false;
+    }
+
+    if (lastTemplate.getClass() != newTemplate.getClass()) {
+      L.d(
+          LogTags.DISTRACTION,
+          "REFRESH check failed. Template type differs (previous: %s, new: %s).",
+          lastTemplateWrapper,
+          templateWrapper);
+      return false;
+    }
+
+    if (isNextTemplateContentRefreshIfSameType || checker.isRefresh(newTemplate, lastTemplate)) {
+      int currentStep =
+          resetTaskStepIfNeeded(lastTemplateStackItem.getStep(), newTemplate.getClass());
+      templateWrapper.setCurrentTaskStep(currentStep);
+      templateWrapper.setRefresh(true);
+      mLastStep = currentStep;
+
+      // We push the new template as a new stack item so that we can keep track of the refresh
+      // stack. This is needed to handle a case where if a template is refreshed across
+      // multiple screens (e.g. same template content, different template ids), when the app
+      // pops back to a previous screen and sends the previous template, the host will
+      // recognize the id in the stack and consider it a back operation. (See b/160892144 for
+      // more context).
+      mTemplateItemStack.push(
+          new TemplateStackItem(
+              templateWrapper.getId(),
+              newTemplate.getClass(),
+              templateWrapper.getCurrentTaskStep()));
+
+      L.d(
+          LogTags.DISTRACTION,
+          "REFRESH detected. Task step currently at %d of %d. %s",
+          templateWrapper.getCurrentTaskStep(),
+          mStepLimit,
+          templateWrapper);
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Checks whether the given {@link TemplateWrapper} is a back operation. Returns {@code true} if
+   * so, {@code false otherwise}.
+   *
+   * <p>A template is considered a back operation if it is of the same template type and the same ID
+   * as a template that is already on the stack. If the input template is deemed to be a back
+   * operation, method will pop any templates on the stack above the target template we are going
+   * back to, and reset the task step count to the value held by the target template.
+   *
+   * @throws FlowViolationException if the target template with the matching ID is of a different
+   *     template type
+   */
+  private boolean validateBackFlow(TemplateWrapper templateWrapper) throws FlowViolationException {
+    String id = templateWrapper.getId();
+    Template template = templateWrapper.getTemplate();
+
+    // This detects the case where the app is popping screens (e.g. going back).
+    // If there is a template with a matching ID and type in the stack, pop everything
+    // above the found item, then update the template and set the task step to the value at that
+    // found item.
+    TemplateStackItem foundItem = findExistingTemplateStackItem(id);
+    if (foundItem != null) {
+      if (foundItem.getTemplateClass() != template.getClass()) {
+        throw new BackFlowViolationException(
+            String.format(
+                "BACK operation failed. Template types differ (previous: %s, new:" + " %s).",
+                foundItem, templateWrapper));
+      }
+
+      // A special case where if the found template is already at the top of stack, then
+      // it is not a back, but a refresh (e.g. an app sending the same template as before).
+      if (foundItem == mTemplateItemStack.peek()) {
+        return false;
+      }
+
+      while (foundItem != mTemplateItemStack.peek()) {
+        mTemplateItemStack.pop();
+      }
+
+      L.d(
+          LogTags.DISTRACTION,
+          "BACK detected. Task step currently at %d of %d. %s",
+          foundItem.getStep(),
+          mStepLimit,
+          templateWrapper);
+
+      // Set the task step back to the value of the found template the app is going back to.
+      int currentStep = resetTaskStepIfNeeded(foundItem.getStep(), template.getClass());
+      templateWrapper.setCurrentTaskStep(currentStep);
+      mLastStep = currentStep;
+
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Returns the {@link TemplateStackItem} currently in the stack with the given ID, or {@code null}
+   * if none is found.
+   */
+  private @Nullable TemplateStackItem findExistingTemplateStackItem(String id) {
+    TemplateStackItem foundItem = null;
+    for (TemplateStackItem stackItem : mTemplateItemStack) {
+      if (stackItem.getTemplateId().equals(id)) {
+        foundItem = stackItem;
+        break;
+      }
+    }
+
+    return foundItem;
+  }
+
+  /**
+   * Validates that we still have budget for a new template, throw otherwise. If it is the last step
+   * in the flow, also validates that only certain template classes are allowed, throw otherwise.
+   */
+  private void throwIfNewTemplateDisallowed(int nextStepToUse, TemplateWrapper templateWrapper)
+      throws OverLimitFlowViolationException {
+    // Check that we still have quota.
+    if (nextStepToUse > mStepLimit) {
+      throw new OverLimitFlowViolationException(
+          String.format("No template allowed after %d templates. %s", mStepLimit, templateWrapper));
+    }
+
+    // Special case for the last step - only certain template types are supported.
+    // 1. For NavigationTemplates, they are consumption view so they will reset the step count.
+    // 2. For SignInTemplates and LongMessageTemplates, they are parked-only and will not
+    // increase the step count.
+    // 3. PaneTemplates and MessageTemplates are the only other two templates that are allowed
+    // at the end of a task.
+    if (nextStepToUse == mStepLimit) {
+      Class<? extends Template> templateClass = templateWrapper.getTemplate().getClass();
+      if (!(templateClass.equals(NavigationTemplate.class)
+          || templateClass.equals(PaneTemplate.class)
+          || templateClass.equals(MessageTemplate.class)
+          || templateClass.equals(SignInTemplate.class)
+          || templateClass.equals(LongMessageTemplate.class))) {
+        throw new OverLimitFlowViolationException(
+            String.format(
+                "Unsupported template type as the last step in a task. %s", templateWrapper));
+      }
+    }
+  }
+
+  private TemplateValidator(int stepLimit) {
+    mStepLimit = stepLimit;
+  }
+
+  /**
+   * Returns the task step that should be used for the next template, resetting it to 1 if a reset
+   * has been requested or if the template is a "consumption view".
+   */
+  private int resetTaskStepIfNeeded(int taskStep, Class<? extends Template> templateClass) {
+    if (mIsReset || isConsumptionView(templateClass)) {
+      taskStep = 1;
+      L.d(LogTags.DISTRACTION, "Resetting task step to %d. %s", taskStep, templateClass.getName());
+      mIsReset = false;
+    }
+
+    return taskStep;
+  }
+
+  /**
+   * Returns whether the given {@link Template} is a "consumption view".
+   *
+   * <p>Consumption views are defined as “sit-and-stay” experiences. In our library's context, these
+   * is the {@link NavigationTemplate}, and can be extended to other templates such as media
+   * playback and in-call view templates in the future when we support them.
+   */
+  private static boolean isConsumptionView(Class<? extends Template> templateClass) {
+    boolean isConsumptionTemplate = NavigationTemplate.class.equals(templateClass);
+    if (isConsumptionTemplate) {
+      L.d(LogTags.DISTRACTION, "Consumption template detected. %s", templateClass.getName());
+    }
+    return isConsumptionTemplate;
+  }
+
+  /** Returns whether the given {@link Template} is a parked-only template. */
+  private static boolean isParkedOnlyTemplate(Class<? extends Template> templateClass) {
+    boolean isParkedOnly =
+        SignInTemplate.class.equals(templateClass)
+            || LongMessageTemplate.class.equals(templateClass);
+    if (isParkedOnly) {
+      L.d(LogTags.DISTRACTION, "Parked only template detected. %s", templateClass.getName());
+    }
+    return isParkedOnly;
+  }
+
+  /** Structure contain the template information to be stored onto the stack. */
+  private static class TemplateStackItem {
+    private final String mTemplateid;
+    private final Class<? extends Template> mTemplateClass;
+    private final int mStep;
+
+    TemplateStackItem(String templateid, Class<? extends Template> templateClass, int step) {
+      mTemplateid = templateid;
+      mTemplateClass = templateClass;
+      mStep = step;
+    }
+
+    String getTemplateId() {
+      return mTemplateid;
+    }
+
+    Class<? extends Template> getTemplateClass() {
+      return mTemplateClass;
+    }
+
+    int getStep() {
+      return mStep;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java
new file mode 100644
index 0000000..706c227
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/CheckerUtils.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.GridItem;
+import androidx.car.app.model.Item;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Toggle;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.List;
+import java.util.Objects;
+
+/** Shared util methods for handling different template checking logic. */
+public class CheckerUtils {
+  /** Returns whether the sizes and string contents of the two lists of items are equal. */
+  public static <T extends Item> boolean itemsHaveSameContent(
+      List<T> itemList1, List<T> itemList2) {
+    if (itemList1.size() != itemList2.size()) {
+      L.d(
+          LogTags.DISTRACTION,
+          "REFRESH check failed. Different item list sizes. Old: %d. New: %d",
+          itemList1.size(),
+          itemList2.size());
+      return false;
+    }
+
+    for (int i = 0; i < itemList1.size(); i++) {
+      T itemObj1 = itemList1.get(i);
+      T itemObj2 = itemList2.get(i);
+
+      if (itemObj1.getClass() != itemObj2.getClass()) {
+        L.d(
+            LogTags.DISTRACTION,
+            "REFRESH check failed. Different item types at index %d. Old: %s. New: %s",
+            i,
+            itemObj1.getClass(),
+            itemObj2.getClass());
+        return false;
+      }
+
+      if (itemObj1 instanceof Row) {
+        if (!rowsHaveSameContent((Row) itemObj1, (Row) itemObj2, i)) {
+          return false;
+        }
+      } else if (itemObj1 instanceof GridItem) {
+        if (!gridItemsHaveSameContent((GridItem) itemObj1, (GridItem) itemObj2, i)) {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+  /** Returns whether the string contents of the two rows are equal. */
+  private static boolean rowsHaveSameContent(Row row1, Row row2, int index) {
+    // Special case for rows with toggles - if the toggle state has changed, then text updates
+    // are allowed.
+    if (toggleStateHasChanged(row1.getToggle(), row2.getToggle())) {
+      return true;
+    }
+
+    if (!carTextsHasSameString(row1.getTitle(), row2.getTitle())) {
+      L.d(
+          LogTags.DISTRACTION,
+          "REFRESH check failed. Different row titles at index %d. Old: %s. New: %s",
+          index,
+          row1.getTitle(),
+          row2.getTitle());
+      return false;
+    }
+
+    return true;
+  }
+
+  /** Returns whether the string contents of the two grid items are equal. */
+  private static boolean gridItemsHaveSameContent(
+      GridItem gridItem1, GridItem gridItem2, int index) {
+    // We only check the item's title - changes in text and image are considered a refresh.
+    if (!carTextsHasSameString(gridItem1.getTitle(), gridItem2.getTitle())) {
+      L.d(
+          LogTags.DISTRACTION,
+          "REFRESH check failed. Different grid item titles at index %d. Old: %s. New:" + " %s",
+          index,
+          gridItem1.getTitle(),
+          gridItem2.getTitle());
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Returns whether the strings of the two {@link CarText}s are the same.
+   *
+   * <p>Spans that are attached to the strings are ignored from the comparison.
+   */
+  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.toString(), carText2.toString());
+  }
+
+  private static boolean toggleStateHasChanged(@Nullable Toggle toggle1, @Nullable Toggle toggle2) {
+    return toggle1 != null && toggle2 != null && toggle1.isChecked() != toggle2.isChecked();
+  }
+
+  private CheckerUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java
new file mode 100644
index 0000000..fecd3f0
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/GridTemplateChecker.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Toggle;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link GridTemplate} */
+public class GridTemplateChecker implements TemplateChecker<GridTemplate> {
+  /**
+   * A new template is considered a refresh of the old if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title has not changed, and 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>
+   */
+  @Override
+  public boolean isRefresh(GridTemplate newTemplate, GridTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is disallowed.
+      return false;
+    }
+    if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+      return false;
+    }
+
+    ItemList oldList = oldTemplate.getSingleList();
+    ItemList newList = newTemplate.getSingleList();
+    if (oldList != null && newList != null) {
+      return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+    }
+
+    return true;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.java
new file mode 100644
index 0000000..a3023f2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/ListTemplateChecker.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import androidx.car.app.model.Item;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.SectionedItemList;
+import androidx.car.app.model.Toggle;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link ListTemplate} */
+public class ListTemplateChecker implements TemplateChecker<ListTemplate> {
+  /**
+   * A new template is considered a refresh of the old if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title has not changed, and 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>
+   */
+  @Override
+  public boolean isRefresh(ListTemplate newTemplate, ListTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is disallowed.
+      return false;
+    }
+
+    if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+      return false;
+    }
+
+    ItemList oldList = oldTemplate.getSingleList();
+    ItemList newList = newTemplate.getSingleList();
+    if (oldList != null && newList != null) {
+      return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+    } else {
+      List<SectionedItemList> oldSectionedList = oldTemplate.getSectionedLists();
+      List<SectionedItemList> newSectionedList = newTemplate.getSectionedLists();
+
+      if (oldSectionedList.size() != newSectionedList.size()) {
+        return false;
+      }
+
+      for (int i = 0; i < newSectionedList.size(); i++) {
+        SectionedItemList newSection = newSectionedList.get(i);
+        SectionedItemList oldSection = oldSectionedList.get(i);
+
+        ItemList oldItemList = oldSection.getItemList();
+        ItemList newItemList = newSection.getItemList();
+        List<Item> oldSubList =
+            oldItemList == null ? Collections.emptyList() : oldItemList.getItems();
+        List<Item> newSubList =
+            newItemList == null ? Collections.emptyList() : newItemList.getItems();
+        if (!Objects.equals(newSection.getHeader(), oldSection.getHeader())
+            || !CheckerUtils.itemsHaveSameContent(oldSubList, newSubList)) {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.java
new file mode 100644
index 0000000..b3c698a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/MessageTemplateChecker.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import androidx.car.app.model.MessageTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link MessageTemplate} */
+public class MessageTemplateChecker implements TemplateChecker<MessageTemplate> {
+  /**
+   * A new template is considered a refresh of a previous one if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title and messages have not changed.
+   * </ul>
+   */
+  @Override
+  public boolean isRefresh(MessageTemplate newTemplate, MessageTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is not considered a refresh.
+      return false;
+    }
+
+    return Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())
+        && Objects.equals(oldTemplate.getDebugMessage(), newTemplate.getDebugMessage())
+        && Objects.equals(oldTemplate.getMessage(), newTemplate.getMessage());
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java
new file mode 100644
index 0000000..596dec5
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/NavigationTemplateChecker.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.navigation.model.NavigationTemplate;
+
+/** A {@link TemplateChecker} implementation for {@link NavigationTemplate} */
+public class NavigationTemplateChecker implements TemplateChecker<NavigationTemplate> {
+  @Override
+  public boolean isRefresh(NavigationTemplate newTemplate, NavigationTemplate oldTemplate) {
+    // Always allow routing template refreshes.
+    return true;
+  }
+
+  @Override
+  public void checkPermissions(Context context, NavigationTemplate newTemplate) {
+    CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java
new file mode 100644
index 0000000..cf07650
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PaneTemplateChecker.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.PaneTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link PaneTemplate} */
+public class PaneTemplateChecker implements TemplateChecker<PaneTemplate> {
+  /**
+   * A new template is considered a refresh of the old if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title has not changed, and 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>
+   */
+  @Override
+  public boolean isRefresh(PaneTemplate newTemplate, PaneTemplate oldTemplate) {
+    Pane oldPane = oldTemplate.getPane();
+    Pane newPane = newTemplate.getPane();
+    if (oldPane.isLoading()) {
+      return true;
+    } else if (newPane.isLoading()) {
+      return false;
+    }
+
+    if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+      return false;
+    }
+
+    return CheckerUtils.itemsHaveSameContent(oldPane.getRows(), newPane.getRows());
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.java
new file mode 100644
index 0000000..edaf6e7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListMapTemplateChecker.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import android.Manifest.permission;
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.PlaceListMapTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link PlaceListMapTemplate} */
+public class PlaceListMapTemplateChecker implements TemplateChecker<PlaceListMapTemplate> {
+  /**
+   * A new template is considered a refresh of the old if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title has not changed, and 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>
+   */
+  @Override
+  public boolean isRefresh(PlaceListMapTemplate newTemplate, PlaceListMapTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is disallowed.
+      return false;
+    }
+
+    if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+      return false;
+    }
+
+    ItemList oldList = oldTemplate.getItemList();
+    ItemList newList = newTemplate.getItemList();
+    if (oldList != null && newList != null) {
+      return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+    }
+
+    return true;
+  }
+
+  @Override
+  public void checkPermissions(Context context, PlaceListMapTemplate newTemplate) {
+    if (newTemplate.isCurrentLocationEnabled()) {
+      CarAppPermission.checkHasPermission(context, permission.ACCESS_FINE_LOCATION);
+    }
+
+    CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.MAP_TEMPLATES);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java
new file mode 100644
index 0000000..7bf534b
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/PlaceListNavigationTemplateChecker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link PlaceListNavigationTemplate} */
+public class PlaceListNavigationTemplateChecker
+    implements TemplateChecker<PlaceListNavigationTemplate> {
+  /**
+   * A new template is considered a refresh of the old if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title has not changed, and 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>
+   */
+  @Override
+  public boolean isRefresh(
+      PlaceListNavigationTemplate newTemplate, PlaceListNavigationTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is disallowed.
+      return false;
+    }
+
+    if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+      return false;
+    }
+
+    ItemList oldList = oldTemplate.getItemList();
+    ItemList newList = newTemplate.getItemList();
+    if (oldList != null && newList != null) {
+      return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+    }
+
+    return true;
+  }
+
+  @Override
+  public void checkPermissions(Context context, PlaceListNavigationTemplate newTemplate) {
+    CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java
new file mode 100644
index 0000000..8c70ac1
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/RoutePreviewNavigationTemplateChecker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link RoutePreviewNavigationTemplate} */
+public class RoutePreviewNavigationTemplateChecker
+    implements TemplateChecker<RoutePreviewNavigationTemplate> {
+  /**
+   * A new template is considered a refresh of the old if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title has not changed, and 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>
+   */
+  @Override
+  public boolean isRefresh(
+      RoutePreviewNavigationTemplate newTemplate, RoutePreviewNavigationTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is disallowed.
+      return false;
+    }
+
+    if (!Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())) {
+      return false;
+    }
+
+    ItemList oldList = oldTemplate.getItemList();
+    ItemList newList = newTemplate.getItemList();
+    if (oldList != null && newList != null) {
+      return CheckerUtils.itemsHaveSameContent(oldList.getItems(), newList.getItems());
+    }
+
+    return true;
+  }
+
+  @Override
+  public void checkPermissions(Context context, RoutePreviewNavigationTemplate newTemplate) {
+    CarAppPermission.checkHasLibraryPermission(context, CarAppPermission.NAVIGATION_TEMPLATES);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.java
new file mode 100644
index 0000000..3de3493
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/SignInTemplateChecker.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import androidx.car.app.model.signin.InputSignInMethod;
+import androidx.car.app.model.signin.SignInTemplate;
+import java.util.Objects;
+
+/** A {@link TemplateChecker} implementation for {@link SignInTemplate} */
+public class SignInTemplateChecker implements TemplateChecker<SignInTemplate> {
+  /**
+   * A new template is considered a refresh of a previous one if:
+   *
+   * <ul>
+   *   <li>The previous template is in a loading state, or
+   *   <li>The template title and instructions have not changed and the input method is the same
+   *       type.
+   * </ul>
+   */
+  @Override
+  public boolean isRefresh(SignInTemplate newTemplate, SignInTemplate oldTemplate) {
+    if (oldTemplate.isLoading()) {
+      // Transition from a previous loading state is allowed.
+      return true;
+    } else if (newTemplate.isLoading()) {
+      // Transition to a loading state is not considered a refresh.
+      return false;
+    }
+    boolean equalSignInMethods =
+        Objects.equals(
+            oldTemplate.getSignInMethod().getClass(), newTemplate.getSignInMethod().getClass());
+
+    if (equalSignInMethods && oldTemplate.getSignInMethod() instanceof InputSignInMethod) {
+      InputSignInMethod oldMethod = (InputSignInMethod) oldTemplate.getSignInMethod();
+      InputSignInMethod newMethod = (InputSignInMethod) newTemplate.getSignInMethod();
+
+      equalSignInMethods =
+          oldMethod.getKeyboardType() == newMethod.getKeyboardType()
+              && Objects.equals(oldMethod.getHint(), newMethod.getHint())
+              && oldMethod.getInputType() == newMethod.getInputType();
+    }
+
+    return Objects.equals(oldTemplate.getTitle(), newTemplate.getTitle())
+        && Objects.equals(oldTemplate.getInstructions(), newTemplate.getInstructions())
+        && Objects.equals(oldTemplate.getAdditionalText(), newTemplate.getAdditionalText())
+        && equalSignInMethods;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java
new file mode 100644
index 0000000..c3474a6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/checkers/TemplateChecker.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.checkers;
+
+import android.content.Context;
+import androidx.car.app.model.Template;
+
+/**
+ * Used for checking template of the specified type within the distraction framework to see if they
+ * meet certain criteria (e.g. whether they are refreshes).
+ *
+ * @param <T> the type of template to check
+ */
+public interface TemplateChecker<T extends Template> {
+  /** Returns whether the {@code newTemplate} is a refresh of the {@code oldTemplate}. */
+  boolean isRefresh(T newTemplate, T oldTemplate);
+
+  /**
+   * Checks that the application has the required permissions for this template.
+   *
+   * @throws SecurityException if the application is missing any required permissions
+   */
+  default void checkPermissions(Context context, T newTemplate) {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java
new file mode 100644
index 0000000..ab54fc7
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ActionsConstraints.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.constraints;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Action;
+import java.util.HashSet;
+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. */
+  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.
+   */
+  public static final ActionsConstraints ACTIONS_CONSTRAINTS_HEADER =
+      ActionsConstraints.builder().setMaxActions(1).addDisallowedAction(Action.TYPE_CUSTOM).build();
+
+  /** Default constraints that should be applied to most templates (2 actions, 1 can have title). */
+  public static final ActionsConstraints ACTIONS_CONSTRAINTS_SIMPLE =
+      ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxCustomTitles(1).build();
+
+  /** Constraints for navigation templates. */
+  public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION =
+      ACTIONS_CONSTRAINTS_CONSERVATIVE
+          .newBuilder()
+          .setMaxActions(4)
+          .setMaxCustomTitles(1)
+          .addRequiredAction(Action.TYPE_CUSTOM)
+          .build();
+
+  /** Constraints for navigation templates. */
+  public static final ActionsConstraints ACTIONS_CONSTRAINTS_NAVIGATION_MAP =
+      ACTIONS_CONSTRAINTS_CONSERVATIVE.newBuilder().setMaxActions(4).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
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Returns a new builder that contains the same data as this {@link ActionsConstraints} instance,
+   */
+  @VisibleForTesting
+  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;
+  }
+
+  /** A builder of {@link ActionsConstraints}. */
+  @VisibleForTesting
+  public static 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. */
+    public Builder setMaxActions(int maxActions) {
+      mMaxActions = maxActions;
+      return this;
+    }
+
+    /** Sets the maximum number of actions with custom titles allowed. */
+    public Builder setMaxCustomTitles(int maxCustomTitles) {
+      mMaxCustomTitles = maxCustomTitles;
+      return this;
+    }
+
+    /** Adds an action type to the set of required types. */
+    public Builder addRequiredAction(int actionType) {
+      mRequiredActionTypes.add(actionType);
+      return this;
+    }
+
+    /** Adds an action type to the set of disallowed types. */
+    public Builder addDisallowedAction(int actionType) {
+      mDisallowedActionTypes.add(actionType);
+      return this;
+    }
+
+    /** TODO(b/174880910): Adding javadoc for AOSP */
+    public ActionsConstraints build() {
+      return new ActionsConstraints(this);
+    }
+
+    private Builder() {}
+
+    private Builder(ActionsConstraints constraints) {
+      mMaxActions = constraints.mMaxActions;
+      mMaxCustomTitles = constraints.mMaxCustomTitles;
+      mRequiredActionTypes.addAll(constraints.mRequiredActionTypes);
+      mDisallowedActionTypes.addAll(constraints.mDisallowedActionTypes);
+    }
+  }
+
+  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.");
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.java
new file mode 100644
index 0000000..6809e08
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarColorConstraints.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.constraints;
+
+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 {
+  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
+          });
+
+  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
+          });
+
+  public static final CarColorConstraints NO_COLOR = CarColorConstraints.create(new int[] {});
+
+  @SuppressWarnings("RestrictTo")
+  @CarColorType
+  private final HashSet<Integer> mAllowedTypes;
+
+  private static CarColorConstraints create(int[] allowedColorTypes) {
+    return new CarColorConstraints(allowedColorTypes);
+  }
+
+  /**
+   * Returns whether the {@link CarColor} meets the constraints' requirement.
+   *
+   * @throws IllegalArgumentException if the color type is not allowed
+   */
+  @SuppressWarnings("RestrictTo")
+  public void validateOrThrow(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) {
+    mAllowedTypes = new HashSet<>();
+    for (int type : allowedColorTypes) {
+      mAllowedTypes.add(type);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java
new file mode 100644
index 0000000..a0ea196
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/CarIconConstraints.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.constraints;
+
+import android.content.ContentResolver;
+
+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. */
+    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. */
+    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 whether 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
+     */
+    public IconCompat checkSupportedIcon(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) {
+        mAllowedTypes = allowedCustomIconTypes;
+    }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java
new file mode 100644
index 0000000..777a1af
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/ConstraintsProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.libraries.apphost.distraction.constraints;
+
+/** Used to provide different limit values for the car app. */
+public interface ConstraintsProvider {
+  /** Provides the max length this car app can use for a content type. */
+  default int getContentLimit(int contentType) {
+    return 0;
+  }
+
+  /** Provides the max size for the template stack for this car app. */
+  default int getTemplateStackMaxSize() {
+    return 0;
+  }
+
+  /** Provides the max length this car app can use for a text view */
+  default int getStringCharacterLimit() {
+    return Integer.MAX_VALUE;
+  }
+
+  /** Returns true if keyboard is restricted for this car app */
+  default boolean isKeyboardRestricted() {
+    return false;
+  }
+
+  /** Returns true if config is restricted for this car app */
+  default boolean isConfigRestricted() {
+    return false;
+  }
+
+  /** Returns true if filtering is restricted for this car app */
+  default boolean isFilteringRestricted() {
+    return false;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java
new file mode 100644
index 0000000..5d68adc
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowConstraints.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.constraints;
+
+/**
+ * Encapsulates the constraints to apply when rendering a {@link androidx.car.app.model.Row} in
+ * different contexts.
+ */
+public class RowConstraints {
+  public static final RowConstraints UNCONSTRAINED = RowConstraints.builder().build();
+
+  /** Conservative constraints for a row. */
+  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. */
+  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 */
+  public static final RowConstraints ROW_CONSTRAINTS_SIMPLE =
+      RowConstraints.builder()
+          .setMaxActionsExclusive(0)
+          .setImageAllowed(true)
+          .setMaxTextLinesPerRow(2)
+          .setToggleAllowed(false)
+          .setOnClickListenerAllowed(true)
+          .build();
+
+  /** The constraints for a full-width row in a list (simple + toggle support). */
+  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;
+  private final CarIconConstraints mCarIconConstraints;
+
+  /** Returns a builder of {@link RowConstraints}. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Returns a builder of {@link RowConstraints} set up with the information from this instance. */
+  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;
+  }
+
+  /** Returns the {@link CarIconConstraints} enforced for the row images. */
+  public CarIconConstraints getCarIconConstraints() {
+    return mCarIconConstraints;
+  }
+
+  private RowConstraints(Builder builder) {
+    mIsOnClickListenerAllowed = builder.mIsOnClickListenerAllowed;
+    mMaxTextLinesPerRow = builder.mMaxTextLines;
+    mMaxActionsExclusive = builder.mMaxActionsExclusive;
+    mIsToggleAllowed = builder.mIsToggleAllowed;
+    mIsImageAllowed = builder.mIsImageAllowed;
+    mCarIconConstraints = builder.mCarIconConstraints;
+  }
+
+  /** A builder of {@link RowConstraints}. */
+  public static 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;
+    private CarIconConstraints mCarIconConstraints = CarIconConstraints.UNCONSTRAINED;
+
+    /** Sets whether a click listener is allowed on the row. */
+    public Builder setOnClickListenerAllowed(boolean isOnClickListenerAllowed) {
+      mIsOnClickListenerAllowed = isOnClickListenerAllowed;
+      return this;
+    }
+
+    /** Sets the maximum number of text lines in a row. */
+    public Builder setMaxTextLinesPerRow(int maxTextLinesPerRow) {
+      mMaxTextLines = maxTextLinesPerRow;
+      return this;
+    }
+
+    /** Sets the maximum number actions to allowed in a row that consists only of actions. */
+    public Builder setMaxActionsExclusive(int maxActionsExclusive) {
+      mMaxActionsExclusive = maxActionsExclusive;
+      return this;
+    }
+
+    /** Sets whether an image can be added to the row. */
+    public Builder setImageAllowed(boolean imageAllowed) {
+      mIsImageAllowed = imageAllowed;
+      return this;
+    }
+
+    /** Sets whether a toggle can be added to the row. */
+    public Builder setToggleAllowed(boolean toggleAllowed) {
+      mIsToggleAllowed = toggleAllowed;
+      return this;
+    }
+
+    /** Sets the {@link CarIconConstraints} enforced for the row images. */
+    public Builder setCarIconConstraints(CarIconConstraints carIconConstraints) {
+      mCarIconConstraints = carIconConstraints;
+      return this;
+    }
+
+    /** Constructs a {@link RowConstraints} object from this builder. */
+    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;
+      mCarIconConstraints = constraints.mCarIconConstraints;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.java
new file mode 100644
index 0000000..0bb9c7d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/distraction/constraints/RowListConstraints.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.distraction.constraints;
+
+import static androidx.car.app.constraints.ConstraintManager.CONTENT_LIMIT_TYPE_LIST;
+import static androidx.car.app.constraints.ConstraintManager.CONTENT_LIMIT_TYPE_PANE;
+import static androidx.car.app.constraints.ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_CONSERVATIVE;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_FULL_LIST;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_PANE;
+import static com.android.car.libraries.apphost.distraction.constraints.RowConstraints.ROW_CONSTRAINTS_SIMPLE;
+
+/** Encapsulates the constraints to apply when rendering a row list under different contexts. */
+public class RowListConstraints {
+  /** Conservative constraints for all types lists. */
+  public static final RowListConstraints ROW_LIST_CONSTRAINTS_CONSERVATIVE =
+      RowListConstraints.builder()
+          .setListContentType(CONTENT_LIMIT_TYPE_LIST)
+          .setMaxActions(0)
+          .setRowConstraints(ROW_CONSTRAINTS_CONSERVATIVE)
+          .setAllowSelectableLists(false)
+          .build();
+
+  /** Default constraints for heterogeneous pane of items, full width. */
+  public static final RowListConstraints ROW_LIST_CONSTRAINTS_PANE =
+      ROW_LIST_CONSTRAINTS_CONSERVATIVE
+          .newBuilder()
+          .setMaxActions(2)
+          .setListContentType(CONTENT_LIMIT_TYPE_PANE)
+          .setRowConstraints(ROW_CONSTRAINTS_PANE)
+          .setAllowSelectableLists(false)
+          .build();
+
+  /** Default constraints for uniform lists of items, no toggles. */
+  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. */
+  public static final RowListConstraints ROW_LIST_CONSTRAINTS_ROUTE_PREVIEW =
+      ROW_LIST_CONSTRAINTS_CONSERVATIVE
+          .newBuilder()
+          .setListContentType(CONTENT_LIMIT_TYPE_ROUTE_LIST)
+          .setRowConstraints(ROW_CONSTRAINTS_SIMPLE)
+          .setAllowSelectableLists(true)
+          .build();
+
+  /** Default constraints for uniform lists of items, full width (simple + toggle support). */
+  public static final RowListConstraints ROW_LIST_CONSTRAINTS_FULL_LIST =
+      ROW_LIST_CONSTRAINTS_CONSERVATIVE
+          .newBuilder()
+          .setRowConstraints(ROW_CONSTRAINTS_FULL_LIST)
+          .setAllowSelectableLists(true)
+          .build();
+
+  private final int mListContentType;
+  private final int mMaxActions;
+  private final RowConstraints mRowConstraints;
+  private final boolean mAllowSelectableLists;
+
+  /** A builder of {@link RowListConstraints}. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Returns a builder of {@link RowListConstraints} set up with the information from this instance.
+   */
+  public Builder newBuilder() {
+    return new Builder(this);
+  }
+
+  /**
+   * Returns the list content type for this constraint.
+   *
+   * <p>This should be one of the content types as defined in {@link
+   * androidx.car.app.constraints.ConstraintManager}.
+   */
+  public int getListContentType() {
+    return mListContentType;
+  }
+
+  /** 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. */
+  public RowConstraints getRowConstraints() {
+    return mRowConstraints;
+  }
+
+  /** Returns whether radio lists are allowed. */
+  public boolean getAllowSelectableLists() {
+    return mAllowSelectableLists;
+  }
+
+  private RowListConstraints(Builder builder) {
+    mMaxActions = builder.mMaxActions;
+    mRowConstraints = builder.mRowConstraints;
+    mAllowSelectableLists = builder.mAllowSelectableLists;
+    mListContentType = builder.mListContentType;
+  }
+
+  /** A builder of {@link RowListConstraints}. */
+  public static class Builder {
+    private int mListContentType;
+    private int mMaxActions;
+    private RowConstraints mRowConstraints = RowConstraints.UNCONSTRAINED;
+    private boolean mAllowSelectableLists;
+
+    /**
+     * Sets the content type for this constraint.
+     *
+     * <p>This should be one of the content types as defined in {@link
+     * androidx.car.app.constraints.ConstraintManager}.
+     */
+    public Builder setListContentType(int contentType) {
+      mListContentType = contentType;
+      return this;
+    }
+
+    /** Sets the maximum number of actions allowed to be added alongside the list. */
+    public Builder setMaxActions(int maxActions) {
+      mMaxActions = maxActions;
+      return this;
+    }
+
+    /** Sets the constraints to apply on individual rows. */
+    public Builder setRowConstraints(RowConstraints rowConstraints) {
+      mRowConstraints = rowConstraints;
+      return this;
+    }
+
+    /** Sets whether radio lists are allowed. */
+    public Builder setAllowSelectableLists(boolean allowSelectableLists) {
+      mAllowSelectableLists = allowSelectableLists;
+      return this;
+    }
+
+    /** Constructs a {@link RowListConstraints} from this builder. */
+    public RowListConstraints build() {
+      return new RowListConstraints(this);
+    }
+
+    private Builder() {}
+
+    private Builder(RowListConstraints constraints) {
+      mMaxActions = constraints.mMaxActions;
+      mRowConstraints = constraints.mRowConstraints;
+      mAllowSelectableLists = constraints.mAllowSelectableLists;
+      mListContentType = constraints.mListContentType;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java
new file mode 100644
index 0000000..db843a9
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditable.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.input;
+
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/** Views that implement this interface are editable by the IME system. */
+public interface CarEditable {
+  /** Notifies that the input connection has been created. */
+  InputConnection onCreateInputConnection(EditorInfo outAttrs);
+
+  /** Sets a listener for events related to input on this car editable. */
+  void setCarEditableListener(CarEditableListener listener);
+
+  /** Sets whether input is enabled. */
+  void setInputEnabled(boolean enabled);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.java
new file mode 100644
index 0000000..2577070
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/CarEditableListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.input;
+
+/**
+ * Callbacks from the {@link CarEditable} to the IME. These methods should be called on the main
+ * thread.
+ */
+public interface CarEditableListener {
+  /**
+   * Indicates that the selection has changed on the current {@link CarEditable}. Note that
+   * selection changes include cursor movements.
+   *
+   * @param oldSelStart the old selection starting index
+   * @param oldSelEnd the old selection ending index
+   * @param newSelStart the new selection starting index
+   * @param newSelEnd the new selection ending index
+   */
+  void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.java
new file mode 100644
index 0000000..896225d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputConfig.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.input;
+
+/** Input configurations of the head unit. */
+public interface InputConfig {
+  /** Returns {@code true} if user can use touchpad to navigate UI, {@code false} otherwise. */
+  boolean hasTouchpadForUiNavigation();
+
+  /** Returns {@code true} if touch input is available, {@code false} otherwise. */
+  boolean hasTouch();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java
new file mode 100644
index 0000000..6d8d13a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/input/InputManager.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.input;
+
+/**
+ * Manages use of the in-car IME. All methods should only be called on the main thread.
+ * TODO(b/174880910): Use @MainThread here.
+ */
+public interface InputManager {
+  /**
+   * Starts input on the requested {@link CarEditable}, showing the IME. If IME input is already
+   * occurring for another view, this call stops input on the previous view and starts input on the
+   * new view.
+   *
+   * <p>This method must only be called from the UI thread. This method should not be called from a
+   * stopped activity.
+   */
+  void startInput(CarEditable view);
+
+  /**
+   * Stops input, hiding the IME. This method fails silently if the calling application didn't
+   * request input and isn't the active IME.
+   *
+   * <p>This function must only be called from the UI thread.
+   */
+  void stopInput();
+
+  /**
+   * Returns {@code true} while the {@link InputManager} is valid. The {@link InputManager} is valid
+   * as long as the activity from which it was obtained has been created and not destroyed.
+   */
+  boolean isValid();
+
+  /**
+   * Returns whether this {@link InputManager} is valid and the IME is active on the given {@link
+   * CarEditable}.
+   */
+  boolean isInputActive();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java
new file mode 100644
index 0000000..8432d92
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/ANRHandlerImpl.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_DISCONNECTED;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_UNBOUND;
+
+import android.content.ComponentName;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.ErrorHandler;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.CarAppApiErrorType;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+
+/** Implementation of an {@link ANRHandler}. */
+public class ANRHandlerImpl implements ANRHandler {
+  private final Handler mHandler = new Handler(Looper.getMainLooper(), new HandlerCallback());
+  private final ComponentName mAppName;
+  private final TelemetryHandler mTelemetryhandler;
+  private final ErrorHandler mErrorHandler;
+
+  /** Creates an {@link ANRHandler} */
+  public static ANRHandler create(
+      ComponentName appName,
+      ErrorHandler errorHandler,
+      TelemetryHandler telemetryHandler,
+      EventManager eventManager) {
+    return new ANRHandlerImpl(appName, errorHandler, telemetryHandler, eventManager);
+  }
+
+  /**
+   * Performs the call and checks for application not responding.
+   *
+   * <p>The ANR check will happen in {@link #ANR_TIMEOUT_MS} milliseconds after calling {@link
+   * ANRCheckingCall#call}.
+   */
+  @Override
+  public void callWithANRCheck(CarAppApi carAppApi, ANRCheckingCall call) {
+    enqueueANRCheck(carAppApi);
+    call.call(
+        new ANRToken() {
+          @Override
+          public void dismiss() {
+            mHandler.removeMessages(carAppApi.ordinal());
+          }
+
+          @Override
+          public CarAppApi getCarAppApi() {
+            return carAppApi;
+          }
+        });
+  }
+
+  private void enqueueANRCheck(CarAppApi carAppApi) {
+    mHandler.removeMessages(carAppApi.ordinal());
+    mHandler.sendMessageDelayed(mHandler.obtainMessage(carAppApi.ordinal()), ANR_TIMEOUT_MS);
+  }
+
+  private void onWaitClicked(CarAppApi carAppApi) {
+    enqueueANRCheck(carAppApi);
+    mErrorHandler.showError(
+        CarAppError.builder(mAppName).setType(CarAppError.Type.ANR_WAITING).build());
+  }
+
+  private void removeAllANRChecks() {
+    for (CarAppApi api : CarAppApi.values()) {
+      mHandler.removeMessages(api.ordinal());
+    }
+  }
+
+  @SuppressWarnings("nullness")
+  private ANRHandlerImpl(
+      ComponentName appName,
+      ErrorHandler errorHandler,
+      TelemetryHandler telemetryHandler,
+      EventManager eventManager) {
+    mAppName = appName;
+    mErrorHandler = errorHandler;
+    mTelemetryhandler = telemetryHandler;
+
+    // Remove any outstanding ANR check whenever the app becomes unbound or crashes.
+    eventManager.subscribeEvent(this, APP_UNBOUND, this::removeAllANRChecks);
+    eventManager.subscribeEvent(this, APP_DISCONNECTED, this::removeAllANRChecks);
+  }
+
+  /** A {@link Handler.Callback} used to implement unbinding. */
+  private class HandlerCallback implements Handler.Callback {
+    @Override
+    public boolean handleMessage(Message msg) {
+      final CarAppApi carAppApi = CarAppApi.values()[msg.what];
+      if (carAppApi == CarAppApi.UNKNOWN_API) {
+        L.w(LogTags.APP_HOST, "Unexpected message for handler %s", msg);
+        return false;
+      } else {
+        // Show an ANR screen allowing the user to wait.
+        // If the user wants to wait, we will show a waiting screen that still allows EXIT.
+        mTelemetryhandler.logCarAppApiFailureTelemetry(mAppName, carAppApi, CarAppApiErrorType.ANR);
+
+        mErrorHandler.showError(
+            CarAppError.builder(mAppName)
+                .setType(CarAppError.Type.ANR_TIMEOUT)
+                .setDebugMessage("ANR API: " + carAppApi.name())
+                .setExtraAction(() -> onWaitClicked(carAppApi))
+                .build());
+
+        return true;
+      }
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml
new file mode 100644
index 0000000..69189b9
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest package="com.android.car.libraries.apphost.internal"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <uses-sdk android:minSdkVersion="23"/>
+</manifest>
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java
new file mode 100644
index 0000000..ba7356c
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/AppDispatcherImpl.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+import static com.android.car.libraries.apphost.logging.TelemetryHandler.getErrorType;
+
+import android.content.ComponentName;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import androidx.car.app.FailureResponse;
+import androidx.car.app.ISurfaceCallback;
+import androidx.car.app.SurfaceContainer;
+import androidx.car.app.model.InputCallbackDelegate;
+import androidx.car.app.model.OnCheckedChangeDelegate;
+import androidx.car.app.model.OnClickDelegate;
+import androidx.car.app.model.OnContentRefreshDelegate;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import androidx.car.app.model.SearchCallbackDelegate;
+import androidx.car.app.navigation.model.PanModeDelegate;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.AppBindingStateProvider;
+import com.android.car.libraries.apphost.common.AppDispatcher;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.ErrorHandler;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.OneWayIPC;
+import com.android.car.libraries.apphost.internal.BlockingOneWayIPC.BlockingResponse;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Class to set up safe remote callbacks to apps.
+ *
+ * <p>App interfaces to client are {@code oneway} so the calling thread does not block waiting for a
+ * response.
+ */
+public class AppDispatcherImpl implements AppDispatcher {
+  /** A request to send over the wire to the app that does not wait for a ANR check. */
+  private interface OneWayIPCNoANRCheck {
+    void send() throws RemoteException;
+  }
+
+  private final ComponentName mAppName;
+  private final ErrorHandler mErrorHandler;
+  private final ANRHandler mANRHandler;
+  private final TelemetryHandler mTelemetryHandler;
+  private final AppBindingStateProvider mAppBindingStateProvider;
+
+  /** Creates an {@link AppDispatcher} instance for an app. */
+  public static AppDispatcher create(
+      ComponentName appName,
+      ErrorHandler errorHandler,
+      ANRHandler anrHandler,
+      TelemetryHandler telemetryHandler,
+      AppBindingStateProvider appBindingStateProvider) {
+    return new AppDispatcherImpl(
+        appName, errorHandler, anrHandler, telemetryHandler, appBindingStateProvider);
+  }
+
+  @Override
+  public void dispatchSurfaceAvailable(
+      ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer) {
+    dispatch(
+        anrToken ->
+            surfaceListener.onSurfaceAvailable(
+                Bundleable.create(surfaceContainer),
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_SURFACE_AVAILABLE);
+  }
+
+  @Override
+  public void dispatchSurfaceDestroyed(
+      ISurfaceCallback surfaceListener, SurfaceContainer surfaceContainer) {
+    // onSurfaceDestroyed is called blocking since the OS expects that whenever we return
+    // the call we are done using the Surface.
+    BlockingResponse<Void> blockingResponse = new BlockingResponse<>();
+    OneWayIPC ipc =
+        anrToken ->
+            surfaceListener.onSurfaceDestroyed(
+                Bundleable.create(surfaceContainer),
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider) {
+                  @Override
+                  public void onSuccess(@Nullable Bundleable response) {
+                    blockingResponse.setResponse(null);
+                    super.onSuccess(response);
+                  }
+
+                  @Override
+                  public void onFailure(Bundleable failureResponse) {
+                    blockingResponse.setResponse(null);
+                    super.onFailure(failureResponse);
+                  }
+                });
+
+    dispatch(new BlockingOneWayIPC<>(ipc, blockingResponse), CarAppApi.ON_SURFACE_DESTROYED);
+  }
+
+  @Override
+  public void dispatchVisibleAreaChanged(ISurfaceCallback surfaceListener, Rect visibleArea) {
+    dispatch(
+        anrToken ->
+            surfaceListener.onVisibleAreaChanged(
+                visibleArea,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_VISIBLE_AREA_CHANGED);
+  }
+
+  @Override
+  public void dispatchStableAreaChanged(ISurfaceCallback surfaceListener, Rect stableArea) {
+    dispatch(
+        anrToken ->
+            surfaceListener.onStableAreaChanged(
+                stableArea,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_STABLE_AREA_CHANGED);
+  }
+
+  @Override
+  public void dispatchOnSurfaceScroll(
+      ISurfaceCallback surfaceListener, float distanceX, float distanceY) {
+    dispatchNoANRCheck(() -> surfaceListener.onScroll(distanceX, distanceY), "onSurfaceScroll");
+  }
+
+  @Override
+  public void dispatchOnSurfaceFling(
+      ISurfaceCallback surfaceListener, float velocityX, float velocityY) {
+    dispatchNoANRCheck(() -> surfaceListener.onFling(velocityX, velocityY), "onSurfaceFling");
+  }
+
+  @Override
+  public void dispatchOnSurfaceScale(
+      ISurfaceCallback surfaceListener, float focusX, float focusY, float scaleFactor) {
+    dispatchNoANRCheck(
+        () -> surfaceListener.onScale(focusX, focusY, scaleFactor), "onSurfaceScale");
+  }
+
+  @Override
+  public void dispatchSearchTextChanged(
+      SearchCallbackDelegate searchCallbackDelegate, String searchText) {
+    dispatch(
+        anrToken ->
+            searchCallbackDelegate.sendSearchTextChanged(
+                searchText,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_SEARCH_TEXT_CHANGED);
+  }
+
+  @Override
+  public void dispatchInputTextChanged(
+      InputCallbackDelegate inputCallbackDelegate, String inputText) {
+    dispatch(
+        anrToken ->
+            inputCallbackDelegate.sendInputTextChanged(
+                inputText,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_INPUT_TEXT_CHANGED);
+  }
+
+  @Override
+  public void dispatchInputSubmitted(
+      InputCallbackDelegate inputCallbackDelegate, String inputText) {
+    dispatch(
+        anrToken ->
+            inputCallbackDelegate.sendInputSubmitted(
+                inputText,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_INPUT_SUBMITTED);
+  }
+
+  @Override
+  public void dispatchSearchSubmitted(
+      SearchCallbackDelegate searchCallbackDelegate, String searchText) {
+    dispatch(
+        anrToken ->
+            searchCallbackDelegate.sendSearchSubmitted(
+                searchText,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_SEARCH_SUBMITTED);
+  }
+
+  @Override
+  public void dispatchItemVisibilityChanged(
+      OnItemVisibilityChangedDelegate onItemVisibilityChangedDelegate,
+      int startIndexInclusive,
+      int endIndexExclusive) {
+    dispatch(
+        anrToken ->
+            onItemVisibilityChangedDelegate.sendItemVisibilityChanged(
+                startIndexInclusive,
+                endIndexExclusive,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_ITEM_VISIBILITY_CHANGED);
+  }
+
+  @Override
+  public void dispatchSelected(OnSelectedDelegate onSelectedDelegate, int index) {
+    dispatch(
+        anrToken ->
+            onSelectedDelegate.sendSelected(
+                index,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_SELECTED);
+  }
+
+  @Override
+  public void dispatchCheckedChanged(
+      OnCheckedChangeDelegate onCheckedChangeDelegate, boolean isChecked) {
+    dispatch(
+        anrToken ->
+            onCheckedChangeDelegate.sendCheckedChange(
+                isChecked,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_CHECKED_CHANGED);
+  }
+
+  @Override
+  public void dispatchPanModeChanged(PanModeDelegate panModeDelegate, boolean isChecked) {
+    dispatch(
+        anrToken ->
+            panModeDelegate.sendPanModeChanged(
+                isChecked,
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_PAN_MODE_CHANGED);
+  }
+
+  @Override
+  public void dispatchClick(OnClickDelegate onClickDelegate) {
+    dispatch(
+        anrToken ->
+            onClickDelegate.sendClick(
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_CLICK);
+  }
+
+  @Override
+  public void dispatchContentRefreshRequest(OnContentRefreshDelegate onContentRefreshDelegate) {
+    dispatch(
+        anrToken ->
+            onContentRefreshDelegate.sendContentRefreshRequested(
+                new OnDoneCallbackStub(
+                    mErrorHandler,
+                    mAppName,
+                    anrToken,
+                    mTelemetryHandler,
+                    mAppBindingStateProvider)),
+        CarAppApi.ON_CLICK);
+  }
+
+  /** Dispatches the given IPC call without checking for an ANR. */
+  private void dispatchNoANRCheck(OneWayIPCNoANRCheck ipc, String callName) {
+    try {
+      ipc.send();
+    } catch (RemoteException e) {
+      mErrorHandler.showError(
+          CarAppError.builder(mAppName)
+              .setCause(e)
+              .setDebugMessage("Remote call " + callName + " failed.")
+              .build());
+    }
+  }
+
+  @Override
+  public void dispatch(OneWayIPC ipc, CarAppApi carAppApi) {
+    dispatch(ipc, mErrorHandler::showError, carAppApi);
+  }
+
+  @Override
+  public void dispatch(OneWayIPC ipc, ExceptionHandler exceptionHandler, CarAppApi carAppApi) {
+    L.d(LogTags.APP_HOST, "Dispatching call %s", carAppApi.name());
+
+    mANRHandler.callWithANRCheck(
+        carAppApi,
+        anrToken -> {
+          try {
+            ipc.send(anrToken);
+          } catch (RemoteException | BundlerException | RuntimeException e) {
+            mTelemetryHandler.logCarAppApiFailureTelemetry(
+                mAppName, carAppApi, getErrorType(new FailureResponse(e)));
+
+            exceptionHandler.handle(
+                CarAppError.builder(mAppName)
+                    .setCause(e)
+                    .setDebugMessage("Remote call " + carAppApi.name() + " failed.")
+                    .build());
+          }
+        });
+  }
+
+  private AppDispatcherImpl(
+      ComponentName appName,
+      ErrorHandler errorHandler,
+      ANRHandler anrHandler,
+      TelemetryHandler telemetryHandler,
+      AppBindingStateProvider appBindingStateProvider) {
+    mAppName = appName;
+    mErrorHandler = errorHandler;
+    mANRHandler = anrHandler;
+    mTelemetryHandler = telemetryHandler;
+    mAppBindingStateProvider = appBindingStateProvider;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.java
new file mode 100644
index 0000000..5d801e8
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/BlockingOneWayIPC.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+import android.os.RemoteException;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+import com.android.car.libraries.apphost.common.OneWayIPC;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link OneWayIPC} that will block waiting for a response from the client before returning.
+ *
+ * <p>Once the client responds set the value received via calling {@link
+ * BlockingResponse#setResponse} on the {@link BlockingResponse} that was supplied.
+ *
+ * <p>When {@link #send} is called, the thread will be blocked up to {@link
+ * BlockingResponse#BLOCKING_MAX_MILLIS} milliseconds, or until the client responds, whichever comes
+ * first.
+ *
+ * <p>If the client does not respond until the timeout, the {@link ANRHandler} will display an ANR
+ * to the user.
+ *
+ * @param <T> the type of the response for the IPC
+ */
+public class BlockingOneWayIPC<T> implements OneWayIPC {
+  /**
+   * A class to block waiting on a response from the client.
+   *
+   * @param <T> the type of the response for the IPC
+   */
+  public static class BlockingResponse<T> {
+    // Set to 4 seconds instead of 5 seconds so the system does not ANR.
+    private static final long BLOCKING_MAX_MILLIS = 4000;
+    private static long sBlockingMaxMillis = BLOCKING_MAX_MILLIS;
+
+    @GuardedBy("this")
+    private boolean mComplete;
+
+    @GuardedBy("this")
+    @Nullable
+    private T mResponse;
+
+    /** Sets the response from the app, releasing any blocking threads. */
+    public void setResponse(@Nullable T response) {
+      synchronized (this) {
+        mResponse = response;
+        mComplete = true;
+        notifyAll();
+      }
+    }
+
+    /** Sets the maximum time to block the IPC for before considering it an ANR, in milliseconds. */
+    @VisibleForTesting
+    public static void setBlockingMaxMillis(long valueForTesting) {
+      sBlockingMaxMillis = valueForTesting;
+    }
+
+    /**
+     * Returns the value provided by calling {@link #setResponse}.
+     *
+     * <p>This method will block waiting for the client to call back before returning.
+     *
+     * <p>The max time method will wait is {@link #BLOCKING_MAX_MILLIS}.
+     */
+    @Nullable
+    private T getBlocking() throws TimeoutException, InterruptedException {
+      synchronized (this) {
+        long startedTimeMillis = System.currentTimeMillis();
+        long waitMillis = sBlockingMaxMillis;
+
+        while (!mComplete && waitMillis > 0) {
+          wait(waitMillis);
+
+          long elapsedMillis = System.currentTimeMillis() - startedTimeMillis;
+          waitMillis = sBlockingMaxMillis - elapsedMillis;
+        }
+        if (!mComplete) {
+          throw new TimeoutException("Response was not set while blocked");
+        }
+
+        return mResponse;
+      }
+    }
+  }
+
+  private final OneWayIPC mOneWayIPC;
+  private final BlockingResponse<T> mBlockingResponse;
+  @Nullable private T mResponse;
+
+  /** Constructs an instance of a {@link BlockingOneWayIPC}. */
+  public BlockingOneWayIPC(OneWayIPC oneWayIPC, BlockingResponse<T> blockingResponse) {
+    mOneWayIPC = oneWayIPC;
+    mBlockingResponse = blockingResponse;
+  }
+
+  @Override
+  public void send(ANRToken anrToken) throws BundlerException, RemoteException {
+    mOneWayIPC.send(anrToken);
+    try {
+      mResponse = mBlockingResponse.getBlocking();
+      anrToken.dismiss();
+    } catch (InterruptedException e) {
+      anrToken.dismiss();
+      throw new IllegalStateException("Exception while waiting for client response.", e);
+    } catch (TimeoutException e) {
+      L.w(LogTags.APP_HOST, e, "Timeout blocking for a client response");
+      // Let the ANR handler handle the ANR by not dismissing the token.
+    }
+  }
+
+  /**
+   * Returns the {@code Response} returned from the {@link Future} provided, or {@code null} if the
+   * app did not respond.
+   *
+   * <p>{@link #send} should be called before calling method, otherwise the result will be {@code
+   * null}.
+   */
+  @Nullable
+  public T getResponse() {
+    return mResponse;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java
new file mode 100644
index 0000000..e142e78
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBinding.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.APP_DISCONNECTED;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import androidx.annotation.AnyThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.AppInfo;
+import androidx.car.app.CarContext;
+import androidx.car.app.HandshakeInfo;
+import androidx.car.app.IAppManager;
+import androidx.car.app.ICarApp;
+import androidx.car.app.ICarHost;
+import androidx.car.app.navigation.INavigationManager;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.core.util.Consumer;
+import androidx.lifecycle.Lifecycle.Event;
+import com.android.car.libraries.apphost.common.ANRHandler.ANRToken;
+import com.android.car.libraries.apphost.common.AppDispatcher;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.CarHostConfig;
+import com.android.car.libraries.apphost.common.IncompatibleApiException;
+import com.android.car.libraries.apphost.common.IntentUtils;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.StatusReporter;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import java.io.PrintWriter;
+import java.security.InvalidParameterException;
+
+/** Manages a binding to the {@link ICarApp} and handles the communication with it. */
+public class CarAppBinding implements StatusReporter {
+
+  private static final int MSG_UNBIND = 1;
+  private static final int MSG_REBIND = 2;
+
+  private enum BindingState {
+    UNBOUND,
+    BINDING,
+    BOUND
+  }
+
+  private final Handler mMainHandler = new Handler(Looper.getMainLooper(), new HandlerCallback());
+  private final ComponentName mAppName;
+  private final ICarHost mCarHost;
+  private final CarAppBindingCallback mCarAppBindingCallback;
+  private final ServiceConnection mServiceConnection = new ServiceConnectionImpl();
+  private final TelemetryHandler mTelemetryHandler;
+
+  // The following fields can be updated by different threads, therefore they are volatile so that
+  // readers use the latest value.
+
+  private volatile TemplateContext mTemplateContext;
+
+  @Nullable private volatile ICarApp mCarApp;
+  @Nullable private volatile IInterface mAppManager;
+  @Nullable private volatile IInterface mNavigationManager;
+
+  @Nullable private volatile Intent mOriginalIntent;
+  @Nullable private volatile ANRToken mANRToken;
+
+  @Nullable private AppInfo mAppInfo;
+
+  /**
+   * The current state of the binding with the client app service. Use {@link
+   * #setBindingState(BindingState)} to update it.
+   */
+  private volatile BindingState mBindingState = BindingState.UNBOUND;
+
+  /**
+   * Creates a {@link CarAppBinding} for binding to and communicating with {@code appName}.
+   *
+   * @param templateContext the context to retrieve template helpers from
+   * @param carHost the host to send to the app when it is bound
+   * @param carAppBindingCallback callback to perform once the app is bound
+   */
+  public static CarAppBinding create(
+      TemplateContext templateContext,
+      ICarHost carHost,
+      CarAppBindingCallback carAppBindingCallback) {
+    return new CarAppBinding(templateContext, carHost, carAppBindingCallback);
+  }
+
+  @Override
+  public void reportStatus(PrintWriter pw, Pii piiHandling) {
+    pw.printf("- state: %s\n", mBindingState.name());
+    mTemplateContext.getCarHostConfig().reportStatus(pw, piiHandling);
+  }
+
+  /** Returns the name of the app this binding is managing. */
+  @AnyThread
+  public ComponentName getAppName() {
+    return mAppName;
+  }
+
+  @Override
+  public String toString() {
+    return "[" + mAppName.flattenToShortString() + ", state: " + mBindingState + "]";
+  }
+
+  /** Sets the {@link TemplateContext} instance attached to this binding. */
+  @AnyThread
+  public void setTemplateContext(TemplateContext templateContext) {
+    mTemplateContext = templateContext;
+
+    AppInfo appInfo = mAppInfo;
+    if (appInfo != null) {
+      try {
+        mTemplateContext.getCarHostConfig().updateNegotiatedApi(appInfo);
+      } catch (IncompatibleApiException exception) {
+        unbind(CarAppError.builder(mAppName).setCause(exception).build());
+      }
+    }
+  }
+
+  /** Binds to the app, if not bound already. */
+  @AnyThread
+  public void bind(Intent binderIntent) {
+    L.d(LogTags.APP_HOST, "Binding to %s with intent %s", this, binderIntent);
+    mMainHandler.removeMessages(MSG_UNBIND);
+    mMainHandler.removeMessages(MSG_REBIND);
+    final Intent originalIntent = IntentUtils.extractOriginalIntent(binderIntent);
+    mOriginalIntent = originalIntent;
+
+    switch (mBindingState) {
+      case UNBOUND:
+        setBindingState(BindingState.BINDING);
+
+        try {
+          // We bind to the app with host's capabilities, which allows the "while-in-use"
+          // permission capabilities (e.g. location) in the app's process for the duration of
+          // the binding.
+          // See go/watevra-nav-location for more information on the process capabilities.
+          if (mTemplateContext
+              .getApplicationContext()
+              .bindService(
+                  binderIntent,
+                  mServiceConnection,
+                  Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) {
+            mTemplateContext
+                .getAnrHandler()
+                .callWithANRCheck(CarAppApi.BIND, (currentAnrToken) -> mANRToken = currentAnrToken);
+          } else {
+            failedToBind(null);
+          }
+        } catch (SecurityException e) {
+          L.e(LogTags.APP_HOST, e, "Cannot bind to the service.");
+          failedToBind(e);
+        }
+
+        return;
+      case BOUND:
+        dispatch(
+            CarContext.CAR_SERVICE,
+            NamedAppServiceCall.create(
+                CarAppApi.ON_NEW_INTENT,
+                (ICarApp carApp, ANRToken anrToken) ->
+                    carApp.onNewIntent(
+                        originalIntent,
+                        new OnDoneCallbackStub(mTemplateContext, anrToken) {
+                          @Override
+                          public void onSuccess(@Nullable Bundleable response) {
+                            super.onSuccess(response);
+                            ThreadUtils.runOnMain(mCarAppBindingCallback::onNewIntentDispatched);
+                          }
+                        })));
+        return;
+      case BINDING:
+        L.d(LogTags.APP_HOST, "Already binding to %s", mAppName);
+    }
+  }
+
+  /** Dispatches the lifecycle call for the given {@code event} to the app. */
+  @AnyThread
+  public void dispatchAppLifecycleEvent(Event event) {
+    L.d(
+        LogTags.APP_HOST,
+        "Dispatching lifecycle event: %s, app: %s",
+        event,
+        mAppName.toShortString());
+
+    dispatch(
+        CarContext.CAR_SERVICE,
+        NamedAppServiceCall.create(
+            CarAppApi.DISPATCH_LIFECYCLE,
+            (ICarApp carApp, ANRToken anrToken) -> {
+              switch (event) {
+                case ON_START:
+                  carApp.onAppStart(new OnDoneCallbackStub(mTemplateContext, anrToken));
+                  return;
+                case ON_RESUME:
+                  carApp.onAppResume(new OnDoneCallbackStub(mTemplateContext, anrToken));
+                  return;
+                case ON_PAUSE:
+                  carApp.onAppPause(new OnDoneCallbackStub(mTemplateContext, anrToken));
+                  return;
+                case ON_STOP:
+                  mMainHandler.removeMessages(MSG_UNBIND);
+                  mMainHandler.removeMessages(MSG_REBIND);
+                  mMainHandler.sendMessageDelayed(
+                      mMainHandler.obtainMessage(MSG_UNBIND),
+                      SECONDS.toMillis(mTemplateContext.getCarHostConfig().getAppUnbindSeconds()));
+                  carApp.onAppStop(new OnDoneCallbackStub(mTemplateContext, anrToken));
+                  return;
+                default:
+                  // fall-through
+              }
+              throw new InvalidParameterException("Received unexpected lifecycle event: " + event);
+            }));
+  }
+
+  /**
+   * Dispatches the {@code call} to the appropriate manager.
+   *
+   * @param managerType one of the CarServiceType as defined in {@link CarContext}
+   * @param call the call to dispatch
+   */
+  @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof ServiceT
+  @AnyThread
+  public <ServiceT extends IInterface> void dispatch(
+      String managerType, NamedAppServiceCall<ServiceT> call) {
+
+    ICarApp carApp = mCarApp;
+
+    if (mBindingState != BindingState.BOUND || carApp == null) {
+      mTemplateContext
+          .getErrorHandler()
+          .showError(
+              CarAppError.builder(mAppName)
+                  .setDebugMessage(
+                      "App is not bound when attempting to get service: "
+                          + managerType
+                          + ", call: "
+                          + call)
+                  .build());
+      return;
+    }
+
+    AppDispatcher appDispatcher = mTemplateContext.getAppDispatcher();
+
+    switch (managerType) {
+      case CarContext.APP_SERVICE:
+        if (mAppManager == null) {
+          dispatchGetManager(
+              appDispatcher,
+              managerType,
+              carApp,
+              manager -> {
+                mAppManager = (IAppManager) manager;
+                dispatchCall(appDispatcher, call, (ServiceT) mAppManager);
+              });
+        } else {
+          dispatchCall(appDispatcher, call, (ServiceT) mAppManager);
+        }
+        break;
+      case CarContext.NAVIGATION_SERVICE:
+        if (mNavigationManager == null) {
+          dispatchGetManager(
+              appDispatcher,
+              managerType,
+              carApp,
+              manager -> {
+                mNavigationManager = (INavigationManager) manager;
+                dispatchCall(appDispatcher, call, (ServiceT) mNavigationManager);
+              });
+        } else {
+          dispatchCall(appDispatcher, call, (ServiceT) mNavigationManager);
+        }
+        break;
+      case CarContext.CAR_SERVICE:
+        dispatchCall(appDispatcher, call, (ServiceT) carApp);
+        break;
+      default:
+        mTemplateContext
+            .getErrorHandler()
+            .showError(
+                CarAppError.builder(mAppName)
+                    .setDebugMessage("No manager was found for type: " + managerType)
+                    .build());
+        break;
+    }
+  }
+
+  /** Returns whether the app is currently bound to. */
+  @AnyThread
+  public boolean isBound() {
+    return mBindingState == BindingState.BOUND;
+  }
+
+  /** Returns whether the binder is in unbound state. */
+  @AnyThread
+  @VisibleForTesting
+  public boolean isUnbound() {
+    return mBindingState == BindingState.UNBOUND;
+  }
+
+  /** Returns the {@link ServiceConnection} instance used by this binding. */
+  @VisibleForTesting
+  public ServiceConnection getServiceConnection() {
+    return mServiceConnection;
+  }
+
+  /**
+   * Unbinds from the app.
+   *
+   * <p>Will not set an error screen.
+   *
+   * <p>If already unbound the call will be a no-op.
+   */
+  @AnyThread
+  public void unbind() {
+    L.d(LogTags.APP_HOST, "Unbinding from %s", this);
+    internalUnbind(null);
+  }
+
+  /**
+   * Unbinds from the app and sets an error screen.
+   *
+   * <p>If already unbound the call will be a no-op.
+   */
+  private void unbind(CarAppError error) {
+    L.d(LogTags.APP_HOST, "Unbinding from %s with error: %s", this, error);
+
+    internalUnbind(error);
+  }
+
+  private void internalUnbind(@Nullable CarAppError errorToShow) {
+    if (mBindingState != BindingState.UNBOUND) {
+      // Run on main thread so that we can unregister from listening for surface changes on
+      // the main thread before an error message is shown which could cause a onSurfaceChanged
+      // callback.
+      ThreadUtils.runOnMain(
+          () -> {
+            mOriginalIntent = null;
+            setBindingState(BindingState.UNBOUND);
+            if (errorToShow != null) {
+              mTemplateContext.getErrorHandler().showError(errorToShow);
+            }
+            resetAppServices();
+            mCarAppBindingCallback.onCarAppUnbound();
+            // Perform tear down logic first, then actually unbind.
+            mTemplateContext.getApplicationContext().unbindService(mServiceConnection);
+          });
+    }
+  }
+
+  private CarAppBinding(
+      TemplateContext templateContext,
+      ICarHost carHost,
+      CarAppBindingCallback carAppBindingCallback) {
+    mTemplateContext = templateContext;
+    mAppName = templateContext.getCarAppPackageInfo().getComponentName();
+    mCarHost = carHost;
+    mCarAppBindingCallback = carAppBindingCallback;
+    mTelemetryHandler = templateContext.getTelemetryHandler();
+  }
+
+  private void resetAppServices() {
+    mCarApp = null;
+    mAppManager = null;
+    mNavigationManager = null;
+  }
+
+  private void setBindingState(BindingState bindingState) {
+    if (mBindingState == bindingState) {
+      return;
+    }
+    BindingState previousState = mBindingState;
+    mBindingState = bindingState;
+    L.d(
+        LogTags.APP_HOST,
+        "Binding state changed from %s to %s for %s",
+        previousState,
+        bindingState,
+        mAppName.flattenToShortString());
+  }
+
+  /**
+   * Retrieves a car service manager from the app
+   *
+   * @param appDispatcher the dispatcher used for making the getManager call
+   * @param managerType one of the CarServiceType as defined in {@link CarContext}
+   * @param carApp the car app to retrieve the manager from
+   * @param callback the callback to trigger on receiving the result from the app
+   */
+  private void dispatchGetManager(
+      AppDispatcher appDispatcher, String managerType, ICarApp carApp, Consumer<Object> callback) {
+    appDispatcher.dispatch(
+        anrToken ->
+            carApp.getManager(
+                managerType,
+                new OnDoneCallbackStub(mTemplateContext, anrToken) {
+                  @Override
+                  public void onSuccess(@Nullable Bundleable response) {
+                    super.onSuccess(checkNotNull(response));
+
+                    try {
+                      callback.accept(response.get());
+                    } catch (BundlerException e) {
+                      mTemplateContext
+                          .getErrorHandler()
+                          .showError(CarAppError.builder(mAppName).setCause(e).build());
+                      return;
+                    }
+                  }
+                }),
+        CarAppApi.GET_MANAGER);
+  }
+
+  @SuppressWarnings("cast.unsafe") // Cannot check if instanceof ServiceT
+  private static <ServiceT extends IInterface> void dispatchCall(
+      AppDispatcher appDispatcher, NamedAppServiceCall<ServiceT> call, ServiceT serviceT) {
+    appDispatcher.dispatch(anrToken -> call.dispatch(serviceT, anrToken), call.getCarAppApi());
+  }
+
+  private final class ServiceConnectionImpl implements ServiceConnection {
+    private boolean mHasConnectedSinceLastBind;
+
+    @Override
+    public void onServiceConnected(ComponentName appName, IBinder service) {
+      L.d(LogTags.APP_HOST, "App service connected: %s", appName.flattenToShortString());
+      ANRToken token = mANRToken;
+      if (token != null) {
+        token.dismiss();
+      }
+      mHasConnectedSinceLastBind = true;
+
+      resetAppServices();
+      mCarApp = ICarApp.Stub.asInterface(service);
+      dispatchGetAppInfo(mCarApp);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName appName) {
+      L.d(LogTags.APP_HOST, "App service disconnected: %s", appName.flattenToShortString());
+
+      if (mHasConnectedSinceLastBind) {
+        mHasConnectedSinceLastBind = false;
+        setBindingState(BindingState.BINDING);
+        resetAppServices();
+        mTemplateContext.getEventManager().dispatchEvent(APP_DISCONNECTED);
+      } else {
+        unbind(
+            CarAppError.builder(appName)
+                .setDebugMessage("The app has crashed multiple times")
+                .build());
+      }
+    }
+
+    @Override
+    public void onBindingDied(ComponentName appName) {
+      L.d(LogTags.APP_HOST, "App binding died: %s", appName.flattenToShortString());
+
+      mMainHandler.removeMessages(MSG_REBIND);
+
+      setBindingState(BindingState.UNBOUND);
+      resetAppServices();
+      mTemplateContext.getEventManager().dispatchEvent(APP_DISCONNECTED);
+
+      mMainHandler.sendMessageDelayed(mMainHandler.obtainMessage(MSG_REBIND), 500);
+    }
+
+    @Override
+    public void onNullBinding(ComponentName name) {
+      unbind(CarAppError.builder(mAppName).setDebugMessage("Null binding from app").build());
+    }
+
+    private void dispatchGetAppInfo(ICarApp carApp) {
+      mTemplateContext
+          .getAppDispatcher()
+          .dispatch(
+              anrToken -> sendAppInfoIPC(carApp, anrToken),
+              CarAppBinding.this::unbind,
+              CarAppApi.GET_APP_VERSION);
+    }
+
+    private void sendAppInfoIPC(ICarApp carApp, ANRToken anrToken) throws RemoteException {
+      carApp.getAppInfo(
+          new OnDoneCallbackStub(mTemplateContext, anrToken) {
+            @Override
+            public void onSuccess(@Nullable Bundleable response) {
+              super.onSuccess(checkNotNull(response));
+              CarHostConfig hostConfig = mTemplateContext.getCarHostConfig();
+              try {
+                AppInfo appInfo = (AppInfo) response.get();
+                mTelemetryHandler.logCarAppTelemetry(
+                    TelemetryEvent.newBuilder(UiAction.CLIENT_SDK_VERSION, mAppName)
+                        .setCarAppSdkVersion(appInfo.getLibraryDisplayVersion()));
+                dispatchOnHandshakeCompleted(carApp, hostConfig.updateNegotiatedApi(appInfo));
+                mAppInfo = appInfo;
+
+              } catch (BundlerException e) {
+                unbind(CarAppError.builder(mAppName).setCause(e).build());
+              } catch (IncompatibleApiException e) {
+                unbind(
+                    CarAppError.builder(mAppName)
+                        .setType(CarAppError.Type.INCOMPATIBLE_CLIENT_VERSION)
+                        .setCause(e)
+                        .build());
+              }
+            }
+          });
+    }
+
+    @SuppressWarnings("RestrictTo")
+    private void dispatchOnHandshakeCompleted(ICarApp carApp, int negotiatedApiLevel) {
+      HandshakeInfo handshakeInfo =
+          new HandshakeInfo(mTemplateContext.getPackageName(), negotiatedApiLevel);
+      mTemplateContext
+          .getAppDispatcher()
+          .dispatch(
+              anrToken ->
+                  carApp.onHandshakeCompleted(
+                      Bundleable.create(handshakeInfo),
+                      new OnDoneCallbackStub(mTemplateContext, anrToken) {
+                        @Override
+                        public void onSuccess(@Nullable Bundleable response) {
+                          super.onSuccess(response);
+                          dispatchOnAppCreate(carApp);
+                        }
+                      }),
+              CarAppBinding.this::unbind,
+              CarAppApi.ON_HANDSHAKE_COMPLETED);
+    }
+
+    private void dispatchOnAppCreate(ICarApp carApp) {
+      mTemplateContext
+          .getAppDispatcher()
+          .dispatch(
+              anrToken ->
+                  carApp.onAppCreate(
+                      mCarHost,
+                      checkNotNull(mOriginalIntent),
+                      mTemplateContext.getResources().getConfiguration(),
+                      new OnDoneCallbackStub(mTemplateContext, anrToken) {
+                        @Override
+                        public void onSuccess(@Nullable Bundleable response) {
+                          super.onSuccess(response);
+                          setBindingState(BindingState.BOUND);
+                          ThreadUtils.runOnMain(mCarAppBindingCallback::onCarAppBound);
+                        }
+
+                        @Override
+                        public void onFailure(Bundleable failureResponse) {
+                          super.onFailure(failureResponse);
+                          L.d(LogTags.APP_HOST, "OnAppCreate Failure");
+                          internalUnbind(null);
+                        }
+                      }),
+              CarAppBinding.this::unbind,
+              CarAppApi.ON_APP_CREATE);
+    }
+  }
+
+  /** A {@link Handler.Callback} used to implement unbinding. */
+  private class HandlerCallback implements Handler.Callback {
+    @Override
+    public boolean handleMessage(Message msg) {
+      if (msg.what == MSG_UNBIND) {
+        if (mTemplateContext.getCarAppPackageInfo().isNavigationApp()) {
+          L.d(LogTags.APP_HOST, "Not unbinding due to the app being a navigation app");
+          return true;
+        }
+        unbind();
+        return true;
+      } else if (msg.what == MSG_REBIND) {
+        bind(new Intent().setComponent(mAppName));
+        return true;
+      }
+
+      L.w(LogTags.APP_HOST, "Unknown message: %s", msg);
+      return false;
+    }
+  }
+
+  /** Updates the internal state and shows an error. */
+  private void failedToBind(@Nullable Throwable cause) {
+    // Set the state to unbound as the binding was unsuccessful.
+    setBindingState(BindingState.UNBOUND);
+
+    CarAppError.Builder builder =
+        CarAppError.builder(mAppName).setDebugMessage("Failed to bind to " + mAppName);
+
+    if (cause != null) {
+      builder.setCause(cause);
+    }
+
+    mTemplateContext.getErrorHandler().showError(builder.build());
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java
new file mode 100644
index 0000000..e1f642a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppBindingCallback.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+/** Provides callbacks for binding-related interaction. */
+public interface CarAppBindingCallback {
+  /** Notifies when the app is bound. */
+  void onCarAppBound();
+
+  /** Notifies that bind was called, when already bound, and onNewIntent was dispatched. */
+  void onNewIntentDispatched();
+
+  /** Notifies when the app is no longer bound. */
+  void onCarAppUnbound();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java
new file mode 100644
index 0000000..661cf23
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/CarAppPackageInfoImpl.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.car.libraries.apphost.common.AppIconLoader;
+import com.android.car.libraries.apphost.common.CarAppColors;
+import com.android.car.libraries.apphost.common.CarAppPackageInfo;
+import com.android.car.libraries.apphost.common.CarColorUtils;
+import com.android.car.libraries.apphost.common.HostResourceIds;
+import java.util.Objects;
+
+/** Provides package information of a 3p car app built using AndroidX Car SDK (go/watevra). */
+public class CarAppPackageInfoImpl implements CarAppPackageInfo {
+  private final Context mContext;
+  private final ComponentName mComponentName;
+  private final boolean mIsNavigationApp;
+  private final AppIconLoader mAppIconLoader;
+  private final HostResourceIds mHostResourceIds;
+
+  private boolean mIsLoaded;
+  @Nullable private CarAppColors mCarAppColors;
+
+  /**
+   * Creates a {@link CarAppPackageInfoImpl} for the application identified by the given {@link
+   * ComponentName}.
+   *
+   * @param context Host context, used to retrieve host resources and configurations
+   * @param componentName Identifier of the car app this instance will provide metadata for
+   * @param isNavigationApp Whether the given car app is a navigation app or not
+   * @param hostResourceIds Host resources, used to retrieve default colors to use in case the app
+   *     doesn't provide their own
+   */
+  public static CarAppPackageInfo create(
+      @NonNull Context context,
+      @NonNull ComponentName componentName,
+      boolean isNavigationApp,
+      @NonNull HostResourceIds hostResourceIds,
+      @NonNull AppIconLoader appIconLoader) {
+    return new CarAppPackageInfoImpl(
+        context, componentName, isNavigationApp, hostResourceIds, appIconLoader);
+  }
+
+  @Override
+  @NonNull
+  public ComponentName getComponentName() {
+    return mComponentName;
+  }
+
+  @NonNull
+  @Override
+  public CarAppColors getAppColors() {
+    ensureLoaded();
+    return Objects.requireNonNull(mCarAppColors);
+  }
+
+  @Override
+  public boolean isNavigationApp() {
+    return mIsNavigationApp;
+  }
+
+  @Override
+  @NonNull
+  public Drawable getRoundAppIcon() {
+    return mAppIconLoader.getRoundAppIcon(mContext, mComponentName);
+  }
+
+  @Override
+  public String toString() {
+    return "[" + mComponentName.flattenToShortString() + ", isNav: " + mIsNavigationApp + "]";
+  }
+
+  @SuppressLint("ResourceType")
+  private void ensureLoaded() {
+    if (mIsLoaded) {
+      return;
+    }
+
+    mCarAppColors = CarColorUtils.resolveAppColor(mContext, mComponentName, mHostResourceIds);
+    mIsLoaded = true;
+  }
+
+  private CarAppPackageInfoImpl(
+      @NonNull Context context,
+      @NonNull ComponentName componentName,
+      boolean isNavigationApp,
+      @NonNull HostResourceIds hostResourceIds,
+      @NonNull AppIconLoader appIconLoader) {
+    mContext = context;
+    mComponentName = componentName;
+    mIsNavigationApp = isNavigationApp;
+    mHostResourceIds = hostResourceIds;
+    mAppIconLoader = appIconLoader;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java
new file mode 100644
index 0000000..10ce9db
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/internal/LocationMediatorImpl.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.internal;
+
+import android.location.Location;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.Place;
+import com.android.car.libraries.apphost.common.EventManager;
+import com.android.car.libraries.apphost.common.EventManager.EventType;
+import com.android.car.libraries.apphost.common.LocationMediator;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link LocationMediator}.
+ *
+ * <p>There's only one set of places available at any given time, so the last writer wins. This
+ * class is not meant to be used with multiple publishers (e.g. shared between multiple apps) so
+ * that is just fine.
+ *
+ * <p>This class is not safe for concurrent access.
+ */
+public class LocationMediatorImpl implements LocationMediator {
+  /** Interface for requesting start and stop of location updates from an app. */
+  public interface AppLocationUpdateRequester {
+    /** Sets whether to get location updates from an app. */
+    void enableLocationUpdates(boolean enabled);
+  }
+
+  @Nullable private CarLocation mCameraAnchor;
+  private List<Place> mCurrentPlaces = ImmutableList.of();
+  private final List<AppLocationListener> mAppLocationListeners = new ArrayList<>();
+  private final EventManager mEventManager;
+  private final AppLocationUpdateRequester mLocationUpdateRequester;
+
+  /** Returns an instance of a {@link LocationMediator}. */
+  public static LocationMediator create(
+      EventManager eventManager, AppLocationUpdateRequester locationUpdateRequester) {
+    return new LocationMediatorImpl(eventManager, locationUpdateRequester);
+  }
+
+  @Override
+  public List<Place> getCurrentPlaces() {
+    return mCurrentPlaces;
+  }
+
+  @Override
+  public void setCurrentPlaces(List<Place> places) {
+    ThreadUtils.ensureMainThread();
+
+    if (mCurrentPlaces.equals(places)) {
+      return;
+    }
+    mCurrentPlaces = places;
+    mEventManager.dispatchEvent(EventType.PLACE_LIST);
+  }
+
+  @Override
+  @Nullable
+  public CarLocation getCameraAnchor() {
+    return mCameraAnchor;
+  }
+
+  @Override
+  public void setCameraAnchor(@Nullable CarLocation cameraAnchor) {
+    mCameraAnchor = cameraAnchor;
+  }
+
+  @Override
+  public void addAppLocationListener(AppLocationListener listener) {
+    if (mAppLocationListeners.isEmpty()) {
+      mLocationUpdateRequester.enableLocationUpdates(true);
+    }
+    mAppLocationListeners.add(listener);
+  }
+
+  @Override
+  public void removeAppLocationListener(AppLocationListener listener) {
+    mAppLocationListeners.remove(listener);
+    if (mAppLocationListeners.isEmpty()) {
+      mLocationUpdateRequester.enableLocationUpdates(false);
+    }
+  }
+
+  @Override
+  public void setAppLocation(Location location) {
+    for (AppLocationListener listener : mAppLocationListeners) {
+      listener.onAppLocationChanged(location);
+    }
+  }
+
+  private LocationMediatorImpl(
+      EventManager eventManager, AppLocationUpdateRequester locationUpdateRequester) {
+    mEventManager = eventManager;
+    mLocationUpdateRequester = locationUpdateRequester;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.java
new file mode 100644
index 0000000..b878286
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/NullUtils.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.lang;
+
+import static com.android.car.libraries.apphost.logging.L.buildMessage;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Supplier;
+
+/**
+ * Utility methods for handling {@code null}able fields and methods.
+ *
+ * <p>These methods should be statically imported and <b>not</b> qualified by class name.
+ */
+public class NullUtils {
+    /**
+     * Returns a {@link Denullerator} with an initial value and type corresponding to the passed
+     * parameter.
+     */
+    public static <T extends Object> Denullerator<T> ifNonNull(@Nullable T reference) {
+        return new Denullerator<>(reference);
+    }
+
+    /**
+     * A reference that can store {@code null} values but from which {@code null} values can never
+     * be retrieved.
+     *
+     * <p>Note that the generic parameter must extend Object explicitly to ensure that the generic
+     * type itself does not match something @Nullable. See
+     * http://go/nullness_troubleshooting#issues-with-type-parameter-annotations
+     *
+     * @param <T> target class
+     */
+    public static class Denullerator<T extends Object> {
+        @Nullable private T mReference;
+
+        /**
+         * New Denullerators should only be created using {@link NullUtils#ifNonNull(Object)} above.
+         */
+        private Denullerator(@Nullable T reference) {
+            mReference = reference;
+        }
+
+        /** Returns a denullerator of a reference value. */
+        public Denullerator<T> otherwiseIfNonNull(@Nullable T reference) {
+            if (mReference == null) {
+                mReference = reference;
+            }
+            return this;
+        }
+
+        /** Returns a denullerators of a reference value supplier. */
+        public Denullerator<T> otherwiseIfNonNull(Supplier<@PolyNull T> referenceSupplier) {
+            if (mReference == null) {
+                mReference = referenceSupplier.get();
+            }
+            return this;
+        }
+
+        /** Return the value if it's not non-null. */
+        public T otherwise(T reference) {
+            return otherwiseIfNonNull(reference).otherwiseThrow();
+        }
+
+        /** Return the value if it's not non-null. */
+        public T otherwise(Supplier<T> referenceSupplier) {
+            return otherwiseIfNonNull(referenceSupplier).otherwiseThrow();
+        }
+
+        /** Returns an exception that values are not non-null */
+        public T otherwiseThrow() {
+            return otherwiseThrow("None of the supplied values were non-null!");
+        }
+
+        /** Returns the reference if it's not non-null. */
+        public T otherwiseThrow(String msg, Object... msgArgs) {
+            if (mReference == null) {
+                throw new NullPointerException(buildMessage(msg, msgArgs));
+            }
+            return mReference;
+        }
+    }
+
+    private NullUtils() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.java
new file mode 100644
index 0000000..2169c21
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/lang/PolyNull.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.lang;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This is an annotation stub to avoid dependencies on annotations that aren't in the Android
+ * platform source tree.
+ */
+@Target({
+  ElementType.ANNOTATION_TYPE,
+  ElementType.CONSTRUCTOR,
+  ElementType.FIELD,
+  ElementType.LOCAL_VARIABLE,
+  ElementType.METHOD,
+  ElementType.PACKAGE,
+  ElementType.PARAMETER,
+  ElementType.TYPE,
+  ElementType.TYPE_PARAMETER,
+  ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface PolyNull {
+  /** This is an enum stub to avoid dependencies. */
+  enum MigrationStatus {
+    IGNORE,
+    WARN,
+    STRICT
+  }
+
+  // These fields maintain API compatibility with annotations that expect arguments.
+  String[] value() default {};
+
+  boolean result() default false;
+
+  String[] expression() default "";
+
+  MigrationStatus status() default MigrationStatus.IGNORE;
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.java
new file mode 100644
index 0000000..5198293
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApi.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+/** Each enum represents one of the Car App Library's possible host to client APIs. */
+// TODO(b/171817245): Remove LINT.IFTT in  copybara
+// LINT.IfChange
+public enum CarAppApi {
+  UNKNOWN_API,
+  GET_APP_VERSION,
+  ON_HANDSHAKE_COMPLETED,
+  GET_MANAGER,
+  GET_TEMPLATE,
+  ON_APP_CREATE,
+  DISPATCH_LIFECYCLE,
+  ON_NEW_INTENT,
+  ON_CONFIGURATION_CHANGED,
+  ON_SURFACE_AVAILABLE,
+  ON_SURFACE_DESTROYED,
+  ON_VISIBLE_AREA_CHANGED,
+  ON_STABLE_AREA_CHANGED,
+  ON_CLICK,
+  ON_SELECTED,
+  ON_SEARCH_TEXT_CHANGED,
+  ON_SEARCH_SUBMITTED,
+  ON_NAVIGATE,
+  STOP_NAVIGATION,
+  ON_RECORDING_STARTED,
+  ON_RECORDING_STOPPED,
+  ON_ITEM_VISIBILITY_CHANGED,
+  ON_CHECKED_CHANGED,
+  ON_BACK_PRESSED,
+  BIND,
+  ON_INPUT_SUBMITTED,
+  ON_INPUT_TEXT_CHANGED,
+  ON_CARHARDWARE_RESULT,
+  ON_PAN_MODE_CHANGED,
+  START_LOCATION_UPDATES,
+  STOP_LOCATION_UPDATES,
+}
+// LINT.ThenChange(//depot/google3/java/com/google/android/apps/auto/components/apphost/internal/\
+//      TelemetryHandlerImpl.java,
+//      //depot/google3/java/com/google/android/apps/automotive/templates/host/di/logging/\
+//      ClearcutTelemetryHandler.java,
+//      //depot/google3/logs/proto/wireless/android/automotive/templates/host/\
+//      android_automotive_templates_host_info.proto)
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java
new file mode 100644
index 0000000..92d09e3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/CarAppApiErrorType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+/** Different errors that may happen due to a Car App Library IPC. */
+// LINT.IfChange
+public enum CarAppApiErrorType {
+  UNKNOWN_ERROR,
+  BUNDLER_EXCEPTION,
+  ILLEGAL_STATE_EXCEPTION,
+  INVALID_PARAMETER_EXCEPTION,
+  SECURITY_EXCEPTION,
+  RUNTIME_EXCEPTION,
+  REMOTE_EXCEPTION,
+  ANR
+}
+// LINT.ThenChange(//depot/google3/java/com/google/android/apps/auto/components/apphost/internal/\
+//      TelemetryHandlerImpl.java,
+//      //depot/google3/java/com/google/android/apps/automotive/templates/host/di/logging/\
+//      ClearcutTelemetryLogger.java,
+//      //depot/google3/logs/proto/wireless/android/automotive/templates/host/\
+//      android_automotive_templates_host_info.proto)
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java
new file mode 100644
index 0000000..ea64917
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/ContentLimitQuery.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+import com.google.auto.value.AutoValue;
+
+/** Internal representation of the content limit queried by car app */
+@AutoValue
+public abstract class ContentLimitQuery {
+
+  /** Returns the content limit type */
+  public abstract int getContentLimitType();
+
+  /** Returns the content limit value */
+  public abstract int getContentLimitValue();
+
+  /**
+   * Returns a new builder of {@link ContentLimitQuery} set up with the information from this event.
+   */
+  public static ContentLimitQuery.Builder newBuilder() {
+    return new AutoValue_ContentLimitQuery.Builder();
+  }
+
+  /** ContentLimit builder. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    /** Sets the content limits {@code type}. */
+    public abstract Builder setContentLimitType(int type);
+
+    /** Sets the content limits {@code value}. */
+    public abstract Builder setContentLimitValue(int value);
+
+    /** Builds a {@link ContentLimitQuery} from this builder. */
+    public ContentLimitQuery build() {
+      return autoBuild();
+    }
+
+    abstract ContentLimitQuery autoBuild();
+  }
+
+  /** Returns a {@link ContentLimitQuery} with given {@code type} and {@code value}. */
+  public static ContentLimitQuery getContentLimitQuery(int type, int value) {
+    return ContentLimitQuery.newBuilder()
+        .setContentLimitValue(value)
+        .setContentLimitType(type)
+        .build();
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java
new file mode 100644
index 0000000..a93469d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/L.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+import java.util.Arrays;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+import java.util.function.Supplier;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Helper class for logging. */
+public final class L {
+  private static final String STRING_MEANING_NULL = "null";
+
+  /** Builds a log message from a message format string and its arguments. */
+  public static String buildMessage(@Nullable String message, @Nullable Object... args) {
+    // If the message is null, ignore the args and return "null";
+    if (message == null) {
+      return STRING_MEANING_NULL;
+    }
+
+    // else if the args are null or 0-length, return message
+    if (args == null || args.length == 0) {
+      try {
+        return String.format(Locale.US, message);
+      } catch (IllegalFormatException ex) {
+        return message;
+      }
+    }
+
+    // Use deepToString to get a more useful representation of any arrays in args
+    for (int i = 0; i < args.length; i++) {
+      if (args[i] != null && args[i].getClass().isArray()) {
+        // Wrap in an array, deepToString, then remove the extra [] from the wrapper. This
+        // allows handling all array types rather than having separate branches for all
+        // primitive array types plus Object[].
+        String string = Arrays.deepToString(new Object[] {args[i]});
+        // Strip the outer [] from the wrapper array.
+        args[i] = string.substring(1, string.length() - 1);
+      }
+    }
+
+    // else try formatting the string.
+    try {
+      return String.format(Locale.US, message, args);
+    } catch (IllegalFormatException ex) {
+      return message + Arrays.deepToString(args);
+    }
+  }
+
+  /**
+   * Log a verbose message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void v(String tag, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.VERBOSE)) {
+      Log.v(tag, message);
+    }
+  }
+
+  /**
+   * Log a verbose message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void v(String tag, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.VERBOSE)) {
+      Log.v(tag, message.get());
+    }
+  }
+
+  /**
+   * Log a verbose message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void v(
+      String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.VERBOSE)) {
+      Log.v(tag, buildMessage(message, args));
+    }
+  }
+
+  /**
+   * Log a verbose message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void v(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.VERBOSE)) {
+      Log.v(tag, message, th);
+    }
+  }
+
+  /**
+   * Log a verbose message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void v(
+      String tag,
+      @Nullable Throwable th,
+      @NonNull @FormatString String message,
+      @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.VERBOSE)) {
+      Log.v(tag, buildMessage(message, args), th);
+    }
+  }
+
+  /**
+   * Log a debug message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void d(String tag, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.DEBUG)) {
+      Log.d(tag, message);
+    }
+  }
+
+  /**
+   * Log a debug message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void d(String tag, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.DEBUG)) {
+      Log.d(tag, message.get());
+    }
+  }
+
+  /**
+   * Log a debug message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void d(
+      String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.DEBUG)) {
+      Log.d(tag, buildMessage(message, args));
+    }
+  }
+
+  /**
+   * Log a debug message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void d(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+
+    if (Log.isLoggable(tag, Log.DEBUG)) {
+      Log.d(tag, message, th);
+    }
+  }
+
+  /**
+   * Log a debug message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void d(
+      String tag,
+      @Nullable Throwable th,
+      @NonNull @FormatString String message,
+      @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.DEBUG)) {
+      Log.d(tag, buildMessage(message, args), th);
+    }
+  }
+
+  /**
+   * Log an info message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void i(String tag, @NonNull @FormatString String message) {
+
+    if (Log.isLoggable(tag, Log.INFO)) {
+      Log.i(tag, message);
+    }
+  }
+
+  /**
+   * Log an info message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void i(
+      String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.INFO)) {
+      Log.i(tag, buildMessage(message, args));
+    }
+  }
+
+  /**
+   * Log an info message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void i(String tag, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.INFO)) {
+      Log.i(tag, message.get());
+    }
+  }
+
+  /**
+   * Log an info message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void i(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.INFO)) {
+      Log.i(tag, message, th);
+    }
+  }
+
+  /**
+   * Log an info message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void i(
+      String tag,
+      @Nullable Throwable th,
+      @NonNull @FormatString String message,
+      @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.INFO)) {
+      Log.i(tag, buildMessage(message, args), th);
+    }
+  }
+
+  /**
+   * Log a warning message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void w(String tag, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.WARN)) {
+      Log.w(tag, message);
+    }
+  }
+
+  /**
+   * Log a warning message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void w(String tag, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.WARN)) {
+      Log.w(tag, message.get());
+    }
+  }
+
+  /**
+   * Log a warning message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void w(String tag, @Nullable Throwable th, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.WARN)) {
+      Log.w(tag, message.get(), th);
+    }
+  }
+
+  /**
+   * Log a warning message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void w(
+      String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.WARN)) {
+      Log.w(tag, buildMessage(message, args));
+    }
+  }
+
+  /**
+   * Log a warning message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void w(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.WARN)) {
+      Log.w(tag, message, th);
+    }
+  }
+
+  /**
+   * Log a warning message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void w(
+      String tag,
+      @Nullable Throwable th,
+      @NonNull @FormatString String message,
+      @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.WARN)) {
+      Log.w(tag, buildMessage(message, args), th);
+    }
+  }
+
+  /**
+   * Log an error message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void e(String tag, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.ERROR)) {
+      Log.e(tag, message);
+    }
+  }
+
+  /**
+   * Log an error message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void e(
+      String tag, @NonNull @FormatString String message, @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.ERROR)) {
+      Log.e(tag, buildMessage(message, args));
+    }
+  }
+
+  /**
+   * Log an error message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   */
+  @FormatMethod
+  public static void e(String tag, @Nullable Throwable th, @NonNull @FormatString String message) {
+    if (Log.isLoggable(tag, Log.ERROR)) {
+      Log.e(tag, message, th);
+    }
+  }
+
+  /**
+   * Log an error message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void e(String tag, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.ERROR)) {
+      Log.e(tag, message.get());
+    }
+  }
+
+  /**
+   * Log an error message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log.
+   * @param message a supplier of the message to log. This will only be executed if the log level
+   *     for the given tag is enabled.
+   */
+  public static void e(String tag, @Nullable Throwable th, @NonNull Supplier<String> message) {
+    if (Log.isLoggable(tag, Log.ERROR)) {
+      Log.e(tag, message.get(), th);
+    }
+  }
+
+  /**
+   * Log an error message.
+   *
+   * @param tag the tag shouldn't be more than 23 characters as {@link Log#isLoggable(String, int)}
+   *     has this restriction.
+   * @param th a throwable to log.
+   * @param message the string message to log. This can also be a string format that's recognized by
+   *     {@link String#format(String, Object...)}. e.g. "%s did something to %s, and %d happened as
+   *     a result".
+   * @param args the formatting args for the previous string.
+   */
+  @FormatMethod
+  public static void e(
+      String tag,
+      @Nullable Throwable th,
+      @NonNull @FormatString String message,
+      @Nullable Object... args) {
+    if (Log.isLoggable(tag, Log.ERROR)) {
+      Log.e(tag, buildMessage(message, args), th);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.java
new file mode 100644
index 0000000..ad48ed3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/LogTags.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+/**
+ * Declares the log tags to use in the app host package.
+ *
+ * <p>These tags are defined at a higher logical and component level, rather than on a strict
+ * per-class basis.
+ *
+ * <p><strong>IMPORTANT</strong>: do not use per-class tags, since those are often way too granular,
+ * hard to manage, and an inferior choice in every way. If you need finer-granularity tags than
+ * those here, consider adding a new one.
+ */
+public abstract class LogTags {
+  /** General purpose tag used for most components. */
+  public static final String APP_HOST = "CarApp.H";
+
+  /** Tag for code related to constraint host. */
+  public static final String CONSTRAINT = APP_HOST + ".Con";
+
+  /** Tag for code related to driver distraction handling. */
+  public static final String DISTRACTION = APP_HOST + ".Dis";
+
+  /** Tag for code related to template handling. */
+  public static final String TEMPLATE = APP_HOST + ".Tem";
+
+  /** Tag for navigation specific host code. */
+  public static final String NAVIGATION = APP_HOST + ".Nav";
+
+  /** Tag for cluster specific host code. */
+  public static final String CLUSTER = APP_HOST + ".Clu";
+
+  /** Tag for renderer service (automotive) specific host code. */
+  public static final String SERVICE = APP_HOST + ".Ser";
+
+  private LogTags() {}
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java
new file mode 100644
index 0000000..ab19b69
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/StatusReporter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+import java.io.PrintWriter;
+
+/** An interface for a component that can contribute status to a bug report. */
+public interface StatusReporter {
+  /** Specifies how to handle PII in a status report. */
+  enum Pii {
+    /** Omit PII from the bug report. */
+    HIDE,
+    /** Show PII in the bug report. */
+    SHOW
+  }
+
+  /**
+   * Writes the status of this component to a bug report.
+   *
+   * @param pw A {@link PrintWriter} to which to write the status.
+   * @param piiHandling How to handle PII in the report.
+   */
+  void reportStatus(PrintWriter pw, Pii piiHandling);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java
new file mode 100644
index 0000000..d5deb3a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryEvent.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+import android.content.ComponentName;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+
+/** Internal representation of a telemetry event. */
+@AutoValue
+public abstract class TelemetryEvent {
+
+  /** Types of actions to be reported */
+  // LINT.IfChange
+  public enum UiAction {
+    APP_START,
+    APP_RUNTIME,
+
+    CAR_APP_API_SUCCESS,
+    CAR_APP_API_FAILURE,
+
+    CAR_APPS_AVAILABLE,
+
+    CLIENT_SDK_VERSION,
+    HOST_SDK_VERSION,
+
+    TEMPLATE_FLOW_LIMIT_EXCEEDED,
+    TEMPLATE_FLOW_INVALID_BACK,
+
+    NAVIGATION_STARTED,
+    NAVIGATION_TRIP_UPDATED,
+    NAVIGATION_ENDED,
+
+    PAN,
+    ROTARY_PAN,
+    FLING,
+    ZOOM,
+
+    ROW_CLICKED,
+    ACTION_STRIP_FAB_CLICKED,
+    ACTION_BUTTON_CLICKED,
+
+    LIST_SIZE,
+    ACTION_STRIP_SIZE,
+    GRID_ITEM_LIST_SIZE,
+
+    ACTION_STRIP_SHOW,
+    ACTION_STRIP_HIDE,
+
+    CONTENT_LIMIT_QUERY,
+
+    HOST_FAILURE_CLUSTER_ICON,
+
+    MINIMIZED_STATE,
+
+    SPEEDBUMPED,
+
+    COLOR_CONTRAST_CHECK_PASSED,
+    COLOR_CONTRAST_CHECK_FAILED,
+  }
+
+  /** Returns the {@link UiAction} that represents the type of action associated with this event. */
+  public abstract UiAction getAction();
+
+  /** Returns the version of the app SDK. */
+  public abstract Optional<String> getCarAppSdkVersion();
+
+  /** Returns the duration of the event, in milliseconds. */
+  public abstract Optional<Long> getDurationMs();
+
+  /** Returns the {@link CarAppApi} associated with the event. */
+  public abstract Optional<CarAppApi> getCarAppApi();
+
+  /** Returns the {@link ComponentName} for the app the event is coming from. */
+  public abstract Optional<ComponentName> getComponentName();
+
+  /** Returns the {@link CarAppApiErrorType} if the event is an error. */
+  public abstract Optional<CarAppApiErrorType> getErrorType();
+
+  /** Returns the position of the event */
+  public abstract Optional<Integer> getPosition();
+
+  /** Returns the count of the loaded item */
+  public abstract Optional<Integer> getItemsLoadedCount();
+
+  /** Returns a {@link ContentLimitQuery} that is used in the car app. */
+  public abstract Optional<ContentLimitQuery> getCarAppContentLimitQuery();
+
+  /** Returns the name of the template used for this event. */
+  public abstract Optional<String> getTemplateClassName();
+
+  /**
+   * Returns a new builder of {@link TelemetryEvent} set up with the given {@link UiAction}, and the
+   * provided {@link ComponentName} set.
+   */
+  public static Builder newBuilder(UiAction action, ComponentName appName) {
+    return newBuilder(action).setComponentName(appName);
+  }
+
+  /** Returns a new builder of {@link TelemetryEvent} set up with the given {@link UiAction} */
+  public static Builder newBuilder(UiAction action) {
+    return new AutoValue_TelemetryEvent.Builder().setAction(action);
+  }
+
+  /** UiLogEvent builder. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Sets the {@link UiAction} that represents the type of action associated with this event. */
+    public abstract Builder setAction(UiAction action);
+
+    /** Sets the version of the app SDK. */
+    public abstract Builder setCarAppSdkVersion(String carAppSdkVersion);
+
+    /** Sets the duration of the event, in milliseconds. */
+    public abstract Builder setDurationMs(long durationMillis);
+
+    /** Sets the {@link CarAppApi} associated with the event. */
+    public abstract Builder setCarAppApi(CarAppApi carAppApi);
+
+    /** Sets the {@link ComponentName} for the app the event is coming from. */
+    public abstract Builder setComponentName(ComponentName componentName);
+
+    /** Sets the {@link CarAppApiErrorType} if the event is an error. */
+    public abstract Builder setErrorType(CarAppApiErrorType errorType);
+
+    /** Sets the position of the event */
+    public abstract Builder setPosition(int position);
+
+    /** Sets the count of the loaded item */
+    public abstract Builder setItemsLoadedCount(int position);
+
+    /** Sets the {@link ContentLimitQuery} that is used in the car app. */
+    public abstract Builder setCarAppContentLimitQuery(ContentLimitQuery constraints);
+
+    /** Sets the class name of the template */
+    public abstract Builder setTemplateClassName(String className);
+
+    /** Builds a {@link TelemetryEvent} from this builder. */
+    public TelemetryEvent build() {
+      return autoBuild();
+    }
+
+    /** Non-visible builder method for AutoValue to implement. */
+    abstract TelemetryEvent autoBuild();
+  }
+  // LINT.ThenChange(//depot/google3/java/com/google/android/apps/auto/components/apphost/\
+  //      internal/TelemetryHandlerImpl.java,
+  //      //depot/google3/java/com/google/android/apps/automotive/templates/host/di/logging/\
+  //      ClearcutTelemetryHandler.java,
+  //      //depot/google3/logs/proto/wireless/android/automotive/templates/host/\
+  //      android_automotive_templates_host_info.proto)
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java
new file mode 100644
index 0000000..9d68892
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/logging/TelemetryHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.logging;
+
+import android.content.ComponentName;
+import androidx.car.app.FailureResponse;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+
+/**
+ * Telemetry service abstraction. Implementations are expected to convert these events to their own
+ * representation and send the information to their corresponding backend.
+ */
+public abstract class TelemetryHandler {
+
+  /** Logs a telemetry event for the given {@link TelemetryEvent.Builder}. */
+  public abstract void logCarAppTelemetry(TelemetryEvent.Builder logEventBuilder);
+
+  /**
+   * Logs a telemetry event with the given {@link UiAction}, the provided {@link ComponentName}, and
+   * the provided {@link CarAppApi}.
+   */
+  public void logCarAppApiSuccessTelemetry(ComponentName appName, CarAppApi carAppApi) {
+    TelemetryEvent.Builder builder =
+        TelemetryEvent.newBuilder(UiAction.CAR_APP_API_SUCCESS, appName).setCarAppApi(carAppApi);
+    logCarAppTelemetry(builder);
+  }
+
+  /**
+   * Logs a telemetry event with the given {@link UiAction}, the provided {@link ComponentName}, the
+   * provided {@link CarAppApi}, and the provided {@link CarAppApiErrorType}.
+   */
+  public void logCarAppApiFailureTelemetry(
+      ComponentName appName, CarAppApi carAppApi, CarAppApiErrorType errorType) {
+    TelemetryEvent.Builder builder =
+        TelemetryEvent.newBuilder(UiAction.CAR_APP_API_FAILURE, appName)
+            .setCarAppApi(carAppApi)
+            .setErrorType(errorType);
+
+    logCarAppTelemetry(builder);
+  }
+
+  /** Helper method for getting the telemetry error type based on a {@link FailureResponse}. */
+  public static CarAppApiErrorType getErrorType(FailureResponse failure) {
+    switch (failure.getErrorType()) {
+      case FailureResponse.BUNDLER_EXCEPTION:
+        return CarAppApiErrorType.BUNDLER_EXCEPTION;
+      case FailureResponse.ILLEGAL_STATE_EXCEPTION:
+        return CarAppApiErrorType.ILLEGAL_STATE_EXCEPTION;
+      case FailureResponse.INVALID_PARAMETER_EXCEPTION:
+        return CarAppApiErrorType.INVALID_PARAMETER_EXCEPTION;
+      case FailureResponse.SECURITY_EXCEPTION:
+        return CarAppApiErrorType.SECURITY_EXCEPTION;
+      case FailureResponse.RUNTIME_EXCEPTION:
+        return CarAppApiErrorType.RUNTIME_EXCEPTION;
+      case FailureResponse.REMOTE_EXCEPTION:
+        return CarAppApiErrorType.REMOTE_EXCEPTION;
+      case FailureResponse.UNKNOWN_ERROR:
+      default:
+        // fall-through
+    }
+    return CarAppApiErrorType.UNKNOWN_ERROR;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java
new file mode 100644
index 0000000..41cd8ea
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationHost.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.nav;
+
+import androidx.car.app.navigation.INavigationHost;
+import androidx.car.app.navigation.model.Trip;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import com.android.car.libraries.apphost.AbstractHost;
+import com.android.car.libraries.apphost.Host;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.StringUtils;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link Host} implementation that handles communication between the client app and the rest of
+ * the host.
+ *
+ * <p>Host services are per app, and live for the duration of a car session.
+ */
+public class NavigationHost extends AbstractHost {
+  private final NavigationManagerDispatcher mDispatcher;
+  private final NavigationHostStub mNavHostStub = new NavigationHostStub();
+
+  @Nullable private Trip mTrip;
+  private final NavigationStateCallback mNavigationStateCallback;
+
+  /** Number of status events to store in a circular buffer for debug reports */
+  private static final int MAX_STATUS_ITEMS = 10;
+
+  /**
+   * A circular buffer which will hold at most {@link #MAX_STATUS_ITEMS}. Items are added at the top
+   * of the list and deleted from the end.
+   */
+  private final ArrayDeque<StatusItem> mStatusItemList = new ArrayDeque<>();
+
+  /** Creates a {@link NavigationHost} instance. */
+  public static NavigationHost create(
+      Object appBinding, TemplateContext templateContext, NavigationStateCallback callback) {
+    return new NavigationHost(
+        NavigationManagerDispatcher.create(appBinding), templateContext, callback);
+  }
+
+  @Override
+  public INavigationHost.Stub getBinder() {
+    assertIsValid();
+    return mNavHostStub;
+  }
+
+  /** Returns the {@link Trip} instance currently set in this host. */
+  @Nullable
+  public Trip getTrip() {
+    assertIsValid();
+    return mTrip;
+  }
+
+  @Override
+  public void reportStatus(PrintWriter pw, Pii piiHandling) {
+    if (mStatusItemList.isEmpty()) {
+      pw.println("No navigation status events stored.");
+      return;
+    }
+    long currentTime = System.currentTimeMillis();
+    // TODO(b/177353816): Update after TableWriter is accessible in the host.
+    pw.printf(
+        "Event   | First Event Delta (millis), Num Consecutive Events, Last Event Delta"
+            + " (millis)\n");
+    for (StatusItem item : mStatusItemList) {
+      pw.printf(
+          "%8s| %s, %d, %s\n",
+          item.mEventType.name(),
+          StringUtils.formatDuration(currentTime - item.mInitialEventMillis),
+          item.mNumConsecutiveEvents,
+          StringUtils.formatDuration(currentTime - item.mFinalEventMillis));
+    }
+  }
+
+  @Override
+  public void onDisconnectedEvent() {
+    mNavigationStateCallback.onNavigationEnded();
+  }
+
+  @Override
+  public void onUnboundEvent() {
+    mNavigationStateCallback.onNavigationEnded();
+  }
+
+  private void setTrip(@Nullable Trip trip) {
+    mTrip = trip;
+  }
+
+  private NavigationHost(
+      NavigationManagerDispatcher dispatcher,
+      TemplateContext templateContext,
+      NavigationStateCallback navigationStateCallback) {
+    super(templateContext, LogTags.NAVIGATION);
+    mNavigationStateCallback = navigationStateCallback;
+    mDispatcher = dispatcher;
+  }
+
+  private final class NavigationHostStub extends INavigationHost.Stub {
+    @Override
+    public void updateTrip(Bundleable tripBundle) {
+      runIfValid(
+          "updateTrip",
+          () -> {
+            try {
+              Trip trip = (Trip) tripBundle.get();
+              ThreadUtils.runOnMain(
+                  () -> {
+                    addStatusItem(StatusItem.EventType.UPDATE);
+                    if (mNavigationStateCallback.onUpdateTrip(trip)) {
+                      setTrip(trip);
+                    }
+                  });
+            } catch (BundlerException e) {
+              mTemplateContext
+                  .getErrorHandler()
+                  .showError(CarAppError.builder(mDispatcher.getAppName()).setCause(e).build());
+            }
+
+            mTemplateContext
+                .getTelemetryHandler()
+                .logCarAppTelemetry(
+                    TelemetryEvent.newBuilder(
+                        UiAction.NAVIGATION_TRIP_UPDATED,
+                        mTemplateContext.getCarAppPackageInfo().getComponentName()));
+          });
+    }
+
+    @Override
+    public void navigationStarted() {
+      runIfValid(
+          "navigationStarted",
+          () -> {
+            L.i(LogTags.NAVIGATION, "%s started navigation", getAppPackageName());
+            ThreadUtils.runOnMain(
+                () -> {
+                  addStatusItem(StatusItem.EventType.START);
+                  mNavigationStateCallback.onNavigationStarted(
+                      () -> {
+                        addStatusItem(StatusItem.EventType.STOP);
+                        mDispatcher.dispatchStopNavigation(mTemplateContext);
+                      });
+                });
+
+            mTemplateContext
+                .getTelemetryHandler()
+                .logCarAppTelemetry(
+                    TelemetryEvent.newBuilder(
+                        UiAction.NAVIGATION_STARTED,
+                        mTemplateContext.getCarAppPackageInfo().getComponentName()));
+          });
+    }
+
+    @Override
+    public void navigationEnded() {
+      if (!isValid()) {
+        L.w(LogTags.NAVIGATION, "Accessed navigationEnded after host became invalidated");
+      }
+      // Run even if not valid so we cleanup state.
+
+      L.i(LogTags.NAVIGATION, "%s ended navigation", getAppPackageName());
+      ThreadUtils.runOnMain(
+          () -> {
+            addStatusItem(StatusItem.EventType.END);
+            mNavigationStateCallback.onNavigationEnded();
+          });
+      mTemplateContext
+          .getTelemetryHandler()
+          .logCarAppTelemetry(
+              TelemetryEvent.newBuilder(
+                  UiAction.NAVIGATION_ENDED,
+                  mTemplateContext.getCarAppPackageInfo().getComponentName()));
+    }
+  }
+
+  private String getAppPackageName() {
+    return mTemplateContext.getCarAppPackageInfo().getComponentName().getPackageName();
+  }
+
+  private void addStatusItem(StatusItem.EventType eventType) {
+    StatusItem item = mStatusItemList.peekFirst();
+    long timestampMillis = System.currentTimeMillis();
+
+    if (item != null && item.mEventType == eventType) {
+      item.appendTimeStamp(System.currentTimeMillis());
+      return;
+    }
+    item = new StatusItem(eventType, timestampMillis);
+    mStatusItemList.addFirst(item);
+    while (mStatusItemList.size() > MAX_STATUS_ITEMS) {
+      mStatusItemList.removeLast();
+    }
+  }
+
+  /**
+   * Entry for reporting the various events that can be reported on.
+   *
+   * <p>Only saves the time of the first and last event along with a count of the total events.
+   */
+  private static class StatusItem {
+    enum EventType {
+      START,
+      UPDATE,
+      END,
+      STOP
+    };
+
+    final EventType mEventType;
+    final long mInitialEventMillis;
+    int mNumConsecutiveEvents;
+    long mFinalEventMillis;
+
+    StatusItem(EventType eventType, long initialEventMillis) {
+      mEventType = eventType;
+      mInitialEventMillis = initialEventMillis;
+      mNumConsecutiveEvents = 1;
+      mFinalEventMillis = initialEventMillis;
+    }
+
+    public void appendTimeStamp(long timestampMillis) {
+      mNumConsecutiveEvents++;
+      mFinalEventMillis = timestampMillis;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java
new file mode 100644
index 0000000..eef041a
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationManagerDispatcher.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.nav;
+
+import androidx.annotation.AnyThread;
+import androidx.car.app.CarContext;
+import androidx.car.app.navigation.INavigationManager;
+import com.android.car.libraries.apphost.ManagerDispatcher;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/** Dispatcher of calls to the {@link INavigationManager}. */
+public class NavigationManagerDispatcher extends ManagerDispatcher<INavigationManager> {
+  /** Creates an instance of {@link NavigationManagerDispatcher}. */
+  public static NavigationManagerDispatcher create(Object appBinding) {
+    return new NavigationManagerDispatcher(appBinding);
+  }
+
+  /** Dispatches {@link INavigationManager#onStopNavigation} to the app. */
+  @AnyThread
+  public void dispatchStopNavigation(TemplateContext templateContext) {
+    dispatch(
+        NamedAppServiceCall.create(
+            CarAppApi.STOP_NAVIGATION,
+            (manager, anrToken) ->
+                manager.onStopNavigation(new OnDoneCallbackStub(templateContext, anrToken))));
+  }
+
+  private NavigationManagerDispatcher(Object appBinding) {
+    super(CarContext.NAVIGATION_SERVICE, appBinding);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.java
new file mode 100644
index 0000000..7d24819
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/nav/NavigationStateCallback.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.nav;
+
+import androidx.car.app.navigation.model.Trip;
+
+/** Handles navigation state change events from {@link NavigationHost}. */
+public interface NavigationStateCallback {
+
+  /**
+   * Notifies that the {@link Trip} set in the {@link NavigationHost} has been updated from the app.
+   */
+  boolean onUpdateTrip(Trip trip);
+
+  /** Notifies that navigation has been started by the app. */
+  void onNavigationStarted(Runnable onNavigationStopRunnable);
+
+  /** Notifies that navigation has been stopped by the app. */
+  void onNavigationEnded();
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java
new file mode 100644
index 0000000..7c1a611
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppHost.java
@@ -0,0 +1,635 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.SURFACE_STABLE_AREA;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.SURFACE_VISIBLE_AREA;
+import static com.android.car.libraries.apphost.logging.TelemetryHandler.getErrorType;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.location.Location;
+import android.os.RemoteException;
+import android.view.Surface;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.CarAppPermission;
+import androidx.car.app.FailureResponse;
+import androidx.car.app.IAppHost;
+import androidx.car.app.IAppManager;
+import androidx.car.app.ISurfaceCallback;
+import androidx.car.app.SurfaceContainer;
+import androidx.car.app.model.GridTemplate;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.LongMessageTemplate;
+import androidx.car.app.model.MessageTemplate;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.PlaceListMapTemplate;
+import androidx.car.app.model.SearchTemplate;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.car.app.model.signin.SignInTemplate;
+import androidx.car.app.navigation.model.NavigationTemplate;
+import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
+import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.car.app.versioning.CarAppApiLevels;
+import com.android.car.libraries.apphost.AbstractHost;
+import com.android.car.libraries.apphost.Host;
+import com.android.car.libraries.apphost.common.ANRHandler;
+import com.android.car.libraries.apphost.common.CarAppError;
+import com.android.car.libraries.apphost.common.LocationMediator;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.distraction.FlowViolationException;
+import com.android.car.libraries.apphost.distraction.OverLimitFlowViolationException;
+import com.android.car.libraries.apphost.distraction.TemplateValidator;
+import com.android.car.libraries.apphost.distraction.checkers.GridTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.ListTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.MessageTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.NavigationTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.PaneTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.PlaceListMapTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.PlaceListNavigationTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.RoutePreviewNavigationTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.SignInTemplateChecker;
+import com.android.car.libraries.apphost.distraction.checkers.TemplateChecker;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import com.android.car.libraries.apphost.view.SurfaceProvider;
+import com.android.car.libraries.apphost.view.SurfaceProvider.SurfaceProviderListener;
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A {@link Host} implementation that handles communication between the client app and the rest of
+ * the host.
+ *
+ * <p>Host services are per app, and live for the duration of a car session.
+ *
+ * <p>A host service keeps a reference to a {@link UIController} object which it delegates UI calls
+ * to, and is responsible for making all the necessary checks to let the operations go through e.g.
+ * check that the backing context (an activity or fragment) is alive, the app is in started state,
+ * etc.
+ *
+ * <p>The {@link UIController} instance may be updated when the backing context is re-created, e.g.
+ * during config changes such as light/dark mode switches.
+ */
+public class AppHost extends AbstractHost {
+  private final IAppHost.Stub mAppHostStub = new AppHostStub();
+  private final AppManagerDispatcher mDispatcher;
+
+  private UIController mUIController;
+  @Nullable private ISurfaceCallback mSurfaceListener;
+  @Nullable private SurfaceContainer mSurfaceContainer;
+  private final AtomicBoolean mIsPendingGetTemplate = new AtomicBoolean(false);
+  private final TemplateValidator mTemplateValidator;
+  private final TelemetryHandler mTelemetryHandler;
+
+  private final SurfaceProvider.SurfaceProviderListener mSurfaceProviderListener =
+      new SurfaceProviderListener() {
+        @Override
+        public void onSurfaceCreated() {
+          L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface created");
+        }
+
+        // call to onVisibleAreaChanged() not allowed on the given receiver.
+        // call to onStableAreaChanged() not allowed on the given receiver.
+        @SuppressWarnings("nullness:method.invocation")
+        @Override
+        public void onSurfaceChanged() {
+          SurfaceContainer container = createOrReuseContainer();
+          mSurfaceContainer = container;
+
+          ISurfaceCallback listener = mSurfaceListener;
+          if (listener != null) {
+            mTemplateContext.getAppDispatcher().dispatchSurfaceAvailable(listener, container);
+          }
+          L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface updated: %s.", container);
+
+          onVisibleAreaChanged();
+          onStableAreaChanged();
+        }
+
+        @Override
+        public void onSurfaceDestroyed() {
+          SurfaceContainer container = createOrReuseContainer();
+          mSurfaceContainer = container;
+
+          ISurfaceCallback listener = mSurfaceListener;
+          if (listener != null) {
+            mTemplateContext.getAppDispatcher().dispatchSurfaceDestroyed(listener, container);
+          }
+
+          mSurfaceContainer = null;
+          L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface destroyed");
+        }
+
+        // call to onSurfaceScroll(float,float) not allowed on the given receiver.
+        @SuppressWarnings("nullness:method.invocation")
+        @Override
+        public void onSurfaceScroll(float distanceX, float distanceY) {
+          AppHost.this.onSurfaceScroll(distanceX, distanceY);
+        }
+
+        // call to onSurfaceScroll(float,float) not allowed on the given receiver.
+        @SuppressWarnings("nullness:method.invocation")
+        @Override
+        public void onSurfaceFling(float velocityX, float velocityY) {
+          AppHost.this.onSurfaceFling(velocityX, velocityY);
+        }
+
+        // call to onSurfaceScroll(float,float) not allowed on the given receiver.
+        @SuppressWarnings("nullness:method.invocation")
+        @Override
+        public void onSurfaceScale(float focusX, float focusY, float scaleFactor) {
+          AppHost.this.onSurfaceScale(focusX, focusY, scaleFactor);
+        }
+
+        private SurfaceContainer createOrReuseContainer() {
+          // dereference of possibly-null reference uiController
+          // dereference of possibly-null reference dispatcher
+          @SuppressWarnings("nullness:dereference.of.nullable")
+          SurfaceProvider provider = mUIController.getSurfaceProvider(mDispatcher.getAppName());
+
+          Surface surface = provider.getSurface();
+          int width = provider.getWidth();
+          int height = provider.getHeight();
+          int dpi = provider.getDpi();
+
+          if (mSurfaceContainer != null
+              && mSurfaceContainer.getSurface() == surface
+              && mSurfaceContainer.getWidth() == width
+              && mSurfaceContainer.getHeight() == height
+              && mSurfaceContainer.getDpi() == dpi) {
+            return mSurfaceContainer;
+          }
+
+          return new SurfaceContainer(surface, width, height, dpi);
+        }
+      };
+
+  /**
+   * Creates a template host service.
+   *
+   * @param uiController the controller to delegate UI calls to. Can be updated with {@link
+   *     #setUIController(UIController)} henceforth
+   * @param appBinding the binding to use to dispatch client calls
+   */
+  public static AppHost create(
+      UIController uiController, Object appBinding, TemplateContext templateContext) {
+    return new AppHost(uiController, AppManagerDispatcher.create(appBinding), templateContext);
+  }
+
+  @Override
+  public IAppHost.Stub getBinder() {
+    assertIsValid();
+    return mAppHostStub;
+  }
+
+  @Override
+  public void onCarAppBound() {
+    super.onCarAppBound();
+
+    updateUiControllerListener();
+    mTemplateValidator.reset();
+    getTemplate();
+  }
+
+  @Override
+  public void onNewIntentDispatched() {
+    super.onNewIntentDispatched();
+
+    getTemplate();
+  }
+
+  @Override
+  public void onBindToApp(Intent intent) {
+    super.onBindToApp(intent);
+
+    if (mTemplateContext.getCarHostConfig().isNewTaskFlowIntent(intent)) {
+      mTemplateValidator.reset();
+    }
+  }
+
+  @Override
+  public void reportStatus(PrintWriter pw, Pii piiHandling) {
+    pw.printf("- flow validator: %s\n", mTemplateValidator);
+    pw.printf("- surface: %s\n", mSurfaceContainer);
+  }
+
+  /** Dispatches an on-back-pressed event. */
+  public void onBackPressed() {
+    assertIsValid();
+    mDispatcher.dispatchOnBackPressed(mTemplateContext);
+  }
+
+  /** Informs the app to start or stop sending location updates. */
+  public void trySetEnableLocationUpdates(boolean enable) {
+    assertIsValid();
+
+    // The enableLocationUpdates API is only available for API level 4+.
+    int apiLevel = mTemplateContext.getCarHostConfig().getNegotiatedApi();
+    if (apiLevel <= CarAppApiLevels.LEVEL_3) {
+      L.e(LogTags.APP_HOST, "Attempt to request location updates for app Api level %s", apiLevel);
+      return;
+    }
+
+    if (enable) {
+      mDispatcher.dispatchStartLocationUpdates(mTemplateContext);
+    } else {
+      mDispatcher.dispatchStopLocationUpdates(mTemplateContext);
+    }
+  }
+
+  /** Dispatches a surface scroll event. */
+  public void onSurfaceScroll(float distanceX, float distanceY) {
+    ISurfaceCallback listener = mSurfaceListener;
+    if (listener != null) {
+      mTemplateContext.getAppDispatcher().dispatchOnSurfaceScroll(listener, distanceX, distanceY);
+      L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface scroll: [%f, %f]", distanceX, distanceY);
+    }
+  }
+
+  /** Dispatches a surface fling event. */
+  public void onSurfaceFling(float velocityX, float velocityY) {
+    ISurfaceCallback listener = mSurfaceListener;
+    if (listener != null) {
+      mTemplateContext.getAppDispatcher().dispatchOnSurfaceFling(listener, velocityX, velocityY);
+      L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface fling: [%f, %f]", velocityX, velocityY);
+    }
+  }
+
+  /** Dispatches a surface scale event. */
+  public void onSurfaceScale(float focusX, float focusY, float scaleFactor) {
+    ISurfaceCallback listener = mSurfaceListener;
+    if (listener != null) {
+      mTemplateContext
+          .getAppDispatcher()
+          .dispatchOnSurfaceScale(listener, focusX, focusY, scaleFactor);
+      L.d(LogTags.TEMPLATE, "SurfaceProvider: Surface scale: [%f]", scaleFactor);
+    }
+  }
+
+  /**
+   * Updates the current {@link UIController}.
+   *
+   * <p>This is normally called when the caller detects that the controller set in the service is
+   * stale due to its backing context being destroyed.
+   */
+  public void setUIController(UIController uiController) {
+    assertIsValid();
+
+    removeUiControllerListener();
+    mUIController = uiController;
+
+    updateUiControllerListener();
+  }
+
+  /** Returns the {@link UIController} attached to this app host. */
+  public UIController getUIController() {
+    assertIsValid();
+    return mUIController;
+  }
+
+  /**
+   * Returns the {@link TemplateValidator} to use to validate whether the templates handled by this
+   * host \ abide by the flow rules.
+   */
+  @VisibleForTesting
+  public TemplateValidator getTemplateValidator() {
+    return mTemplateValidator;
+  }
+
+  /** Registers a {@link TemplateChecker} for a host-only {@link Template}. */
+  public <T extends Template> void registerHostTemplateChecker(
+      Class<T> templateClass, TemplateChecker<T> templateChecker) {
+    mTemplateValidator.registerTemplateChecker(templateClass, templateChecker);
+  }
+
+  @Override
+  public void setTemplateContext(TemplateContext templateContext) {
+    removeEventSubscriptions();
+
+    super.setTemplateContext(templateContext);
+    templateContext.registerAppHostService(TemplateValidator.class, mTemplateValidator);
+    updateEventSubscriptions();
+  }
+
+  @Override
+  public void onDisconnectedEvent() {
+    removeUiControllerListener();
+  }
+
+  private void getTemplate() {
+    boolean wasPendingTemplate = mIsPendingGetTemplate.getAndSet(true);
+    if (wasPendingTemplate) {
+      // Ignore extra invalidate calls between templates being returned.
+      return;
+    }
+
+    mDispatcher.dispatchGetTemplate(this::getTemplateAppServiceCall);
+  }
+
+  private void getTemplateAppServiceCall(IAppManager manager, ANRHandler.ANRToken anrToken)
+      throws RemoteException {
+    manager.getTemplate(
+        new OnDoneCallbackStub(mTemplateContext, anrToken) {
+          @Override
+          public void onSuccess(@Nullable Bundleable response) {
+            super.onSuccess(response);
+            mIsPendingGetTemplate.set(false);
+            ComponentName appName = mDispatcher.getAppName();
+
+            RuntimeException toThrow = null;
+            try {
+              TemplateWrapper wrapper = (TemplateWrapper) checkNotNull(response).get();
+
+              // This checks whether this template meets our task flow
+              // restriction guideline and will throw if the template should not
+              // be added.
+              mTemplateValidator.validateFlow(wrapper);
+              mTemplateValidator.validateHasRequiredPermissions(mTemplateContext, wrapper);
+
+              mUIController.setTemplate(appName, wrapper);
+            } catch (BundlerException e) {
+              mTelemetryHandler.logCarAppApiFailureTelemetry(
+                  appName, CarAppApi.GET_TEMPLATE, getErrorType(new FailureResponse(e)));
+
+              mTemplateContext
+                  .getErrorHandler()
+                  .showError(
+                      CarAppError.builder(appName)
+                          .setCause(e)
+                          .setDebugMessage("Invalid template")
+                          .build());
+            } catch (SecurityException e) {
+              mTelemetryHandler.logCarAppApiFailureTelemetry(
+                  appName, CarAppApi.GET_TEMPLATE, getErrorType(new FailureResponse(e)));
+
+              mTemplateContext
+                  .getErrorHandler()
+                  .showError(
+                      CarAppError.builder(appName)
+                          .setCause(e)
+                          .setType(CarAppError.Type.MISSING_PERMISSION)
+                          .build());
+              toThrow = e;
+            } catch (FlowViolationException e) {
+              mTelemetryHandler.logCarAppTelemetry(
+                  TelemetryEvent.newBuilder(
+                      e instanceof OverLimitFlowViolationException
+                          ? UiAction.TEMPLATE_FLOW_LIMIT_EXCEEDED
+                          : UiAction.TEMPLATE_FLOW_INVALID_BACK,
+                      appName));
+
+              mTemplateContext
+                  .getErrorHandler()
+                  .showError(
+                      CarAppError.builder(appName)
+                          .setCause(e)
+                          .setDebugMessage("Template flow restrictions violated")
+                          .build());
+              toThrow = new IllegalStateException(e);
+            } catch (RuntimeException e) {
+              mTelemetryHandler.logCarAppApiFailureTelemetry(
+                  appName, CarAppApi.GET_TEMPLATE, getErrorType(new FailureResponse(e)));
+
+              mTemplateContext
+                  .getErrorHandler()
+                  .showError(CarAppError.builder(appName).setCause(e).build());
+              toThrow = e;
+            }
+
+            if (toThrow != null) {
+              // Crash the client process if the template returned does not pass validations.
+              throw toThrow;
+            }
+          }
+
+          @Override
+          public void onFailure(Bundleable failureResponse) {
+            super.onFailure(failureResponse);
+            mIsPendingGetTemplate.set(false);
+          }
+        });
+  }
+
+  /**
+   * Dispatches a call to the template app if the surface has been created and there is a visible
+   * area available.
+   */
+  private void onVisibleAreaChanged() {
+    // Do not fire the visible area changed event until at least after the surfaceContainer
+    // is created, which is triggered by the onSurfaceChanged callback.
+    if (mSurfaceContainer == null) {
+      return;
+    }
+
+    Rect visibleArea = mTemplateContext.getSurfaceInfoProvider().getVisibleArea();
+    if (visibleArea == null) {
+      return;
+    }
+    ISurfaceCallback listener = mSurfaceListener;
+    if (listener != null) {
+      mTemplateContext.getAppDispatcher().dispatchVisibleAreaChanged(listener, visibleArea);
+    }
+    L.d(LogTags.TEMPLATE, "SurfaceProvider: onVisibleAreaChanged: visibleArea: [%s]", visibleArea);
+  }
+
+  /**
+   * Dispatches a call to the template app if the surface has been created and there is a stable
+   * area visible.
+   */
+  private void onStableAreaChanged() {
+    // Do not fire the Insets changed event until at least after the surfaceContainer
+    // is created, which is triggered by the onSurfaceChanged callback.
+    if (mSurfaceContainer == null) {
+      return;
+    }
+
+    Rect stableArea = mTemplateContext.getSurfaceInfoProvider().getStableArea();
+    if (stableArea == null) {
+      return;
+    }
+    ISurfaceCallback listener = mSurfaceListener;
+    if (listener != null) {
+      mTemplateContext.getAppDispatcher().dispatchStableAreaChanged(listener, stableArea);
+    }
+    L.d(LogTags.DISTRACTION, "SurfaceProvider: onStableAreaChanged: stableArea: [%s]", stableArea);
+  }
+
+  private void registerTemplateValidators() {
+    mTemplateValidator.registerTemplateChecker(GridTemplate.class, new GridTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(ListTemplate.class, new ListTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(MessageTemplate.class, new MessageTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(
+        NavigationTemplate.class, new NavigationTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(PaneTemplate.class, new PaneTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(
+        PlaceListMapTemplate.class, new PlaceListMapTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(
+        PlaceListNavigationTemplate.class, new PlaceListNavigationTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(
+        RoutePreviewNavigationTemplate.class, new RoutePreviewNavigationTemplateChecker());
+    mTemplateValidator.registerTemplateChecker(SignInTemplate.class, new SignInTemplateChecker());
+
+    // Templates that don't require refresh and permission checks.
+    mTemplateValidator.registerTemplateChecker(
+        LongMessageTemplate.class, (newTemplate, oldTemplate) -> true);
+    mTemplateValidator.registerTemplateChecker(
+        SearchTemplate.class, (newTemplate, oldTemplate) -> true);
+  }
+
+  private void updateUiControllerListener() {
+    SurfaceProvider surfaceProvider =
+        mUIController.getSurfaceProvider(
+            mTemplateContext.getCarAppPackageInfo().getComponentName());
+    if (surfaceProvider == null) {
+      // We should always be able to access the surface provider at the point where the ui
+      // controller is set.
+      throw new IllegalStateException(
+          "Can't get surface provider for "
+              + mTemplateContext.getCarAppPackageInfo().getComponentName().flattenToShortString());
+    }
+    surfaceProvider.setListener(mSurfaceProviderListener);
+  }
+
+  private void removeUiControllerListener() {
+    // Remove any outstanding surface listeners whenever the app crashes, otherwise the listener
+    // may send onSurfaceDestroyed calls when the app is not bound.
+    mUIController
+        .getSurfaceProvider(mTemplateContext.getCarAppPackageInfo().getComponentName())
+        .setListener(null);
+  }
+
+  private void updateEventSubscriptions() {
+    mTemplateContext
+        .getEventManager()
+        .subscribeEvent(this, SURFACE_VISIBLE_AREA, AppHost.this::onVisibleAreaChanged);
+    mTemplateContext
+        .getEventManager()
+        .subscribeEvent(this, SURFACE_STABLE_AREA, AppHost.this::onStableAreaChanged);
+  }
+
+  private void removeEventSubscriptions() {
+    mTemplateContext.getEventManager().unsubscribeEvent(this, SURFACE_VISIBLE_AREA);
+    mTemplateContext.getEventManager().unsubscribeEvent(this, SURFACE_STABLE_AREA);
+  }
+
+  @SuppressWarnings("nullness")
+  private AppHost(
+      UIController uiController, AppManagerDispatcher dispatcher, TemplateContext templateContext) {
+    super(templateContext, LogTags.APP_HOST);
+    mUIController = uiController;
+    mDispatcher = dispatcher;
+    mTemplateValidator =
+        TemplateValidator.create(
+            templateContext.getConstraintsProvider().getTemplateStackMaxSize());
+    mTelemetryHandler = templateContext.getTelemetryHandler();
+
+    templateContext.registerAppHostService(TemplateValidator.class, mTemplateValidator);
+
+    registerTemplateValidators();
+    updateUiControllerListener();
+    updateEventSubscriptions();
+  }
+
+  /**
+   * A {@link IAppHost.Stub} implementation that used to receive calls to the app host API from the
+   * client.
+   */
+  private final class AppHostStub extends IAppHost.Stub {
+    @Override
+    public void invalidate() {
+      runIfValid("invalidate", AppHost.this::getTemplate);
+    }
+
+    @Override
+    public void showToast(CharSequence text, int duration) {
+      ThreadUtils.runOnMain(
+          () ->
+              runIfValid(
+                  "showToast",
+                  () -> mTemplateContext.getToastController().showToast(text, duration)));
+    }
+
+    @Override
+    public void setSurfaceCallback(@Nullable ISurfaceCallback listener) {
+      runIfValid(
+          "setSurfaceCallback",
+          () -> {
+            ComponentName appName = mDispatcher.getAppName();
+            L.d(LogTags.TEMPLATE, "setSurfaceListener for %s", appName);
+
+            Context appConfigurationContext = mTemplateContext.getAppConfigurationContext();
+            if (appConfigurationContext == null) {
+              L.e(LogTags.TEMPLATE, "App configuration context is null");
+              return;
+            }
+
+            try {
+              CarAppPermission.checkHasLibraryPermission(
+                  appConfigurationContext, CarAppPermission.ACCESS_SURFACE);
+            } catch (SecurityException e) {
+              // Catch the Exception here to log in host before throwing to the client
+              // app.
+              L.w(
+                  LogTags.TEMPLATE,
+                  e,
+                  "App %s trying to access surface when the permission was not" + " granted",
+                  appName);
+
+              throw new SecurityException(e);
+            }
+
+            ThreadUtils.runOnMain(
+                () -> {
+                  mSurfaceListener = listener;
+                  if (mSurfaceListener == null) {
+                    return;
+                  }
+
+                  if (mSurfaceContainer != null) {
+                    mSurfaceProviderListener.onSurfaceChanged();
+                  }
+                });
+          });
+    }
+
+    @Override
+    public void sendLocation(Location location) {
+      ThreadUtils.runOnMain(
+          () ->
+              runIfValid(
+                  "sendLocation",
+                  () ->
+                      Objects.requireNonNull(
+                              mTemplateContext.getAppHostService(LocationMediator.class))
+                          .setAppLocation(location)));
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java
new file mode 100644
index 0000000..a1a6ccb
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/AppManagerDispatcher.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.MainThread;
+import androidx.car.app.CarContext;
+import androidx.car.app.IAppManager;
+import com.android.car.libraries.apphost.ManagerDispatcher;
+import com.android.car.libraries.apphost.common.AppServiceCall;
+import com.android.car.libraries.apphost.common.NamedAppServiceCall;
+import com.android.car.libraries.apphost.common.OnDoneCallbackStub;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.CarAppApi;
+
+/** Dispatcher of calls to the {@link IAppManager}. */
+public class AppManagerDispatcher extends ManagerDispatcher<IAppManager> {
+  /** Creates an instance of {@link AppManagerDispatcher} with the given app binding object. */
+  public static AppManagerDispatcher create(Object appBinding) {
+    return new AppManagerDispatcher(appBinding);
+  }
+
+  /** Dispatches {@link IAppManager#getTemplate} to the app. */
+  @AnyThread
+  public void dispatchGetTemplate(AppServiceCall<IAppManager> getTemplateCall) {
+    dispatch(NamedAppServiceCall.create(CarAppApi.GET_TEMPLATE, getTemplateCall));
+  }
+
+  /** Dispatches {@link IAppManager#onBackPressed} to the app. */
+  @AnyThread
+  public void dispatchOnBackPressed(TemplateContext templateContext) {
+    dispatch(
+        NamedAppServiceCall.create(
+            CarAppApi.ON_BACK_PRESSED,
+            (manager, anrToken) ->
+                manager.onBackPressed(new OnDoneCallbackStub(templateContext, anrToken))));
+  }
+
+  /** Dispatches {@link IAppManager#startLocationUpdates} to the app. */
+  @MainThread
+  public void dispatchStartLocationUpdates(TemplateContext templateContext) {
+    dispatch(
+        NamedAppServiceCall.create(
+            CarAppApi.START_LOCATION_UPDATES,
+            (manager, anrToken) ->
+                manager.startLocationUpdates(new OnDoneCallbackStub(templateContext, anrToken))));
+  }
+
+  /** Dispatches {@link IAppManager#stopLocationUpdates} to the app. */
+  @MainThread
+  public void dispatchStopLocationUpdates(TemplateContext templateContext) {
+    dispatch(
+        NamedAppServiceCall.create(
+            CarAppApi.STOP_LOCATION_UPDATES,
+            (manager, anrToken) ->
+                manager.stopLocationUpdates(new OnDoneCallbackStub(templateContext, anrToken))));
+  }
+
+  private AppManagerDispatcher(Object appBinding) {
+    super(CarContext.APP_SERVICE, appBinding);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java
new file mode 100644
index 0000000..bdebe3c
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/ConstraintHost.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template;
+
+import androidx.car.app.constraints.IConstraintHost;
+import com.android.car.libraries.apphost.AbstractHost;
+import com.android.car.libraries.apphost.Host;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.ContentLimitQuery;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+
+/**
+ * A {@link Host} implementation that handles constraints enforced on the connecting app.
+ *
+ * <p>Host services are per app, and live for the duration of a car session.
+ */
+public final class ConstraintHost extends AbstractHost {
+  private final IConstraintHost.Stub mHostStub = new ConstraintHostStub();
+
+  /** Creates a template host service. */
+  public static ConstraintHost create(TemplateContext templateContext) {
+    return new ConstraintHost(templateContext);
+  }
+
+  @Override
+  public IConstraintHost.Stub getBinder() {
+    assertIsValid();
+    return mHostStub;
+  }
+
+  private ConstraintHost(TemplateContext templateContext) {
+    super(templateContext, LogTags.CONSTRAINT);
+  }
+
+  /**
+   * A {@link IConstraintHost.Stub} implementation that used to receive calls to the constraint host
+   * API from the client.
+   */
+  private final class ConstraintHostStub extends IConstraintHost.Stub {
+    @Override
+    public int getContentLimit(int contentType) {
+      if (!isValid()) {
+        L.w(LogTags.CONSTRAINT, "Accessed getContentLimit after host became invalidated");
+      }
+      int contentValue = mTemplateContext.getConstraintsProvider().getContentLimit(contentType);
+      mTemplateContext
+          .getTelemetryHandler()
+          .logCarAppTelemetry(
+              TelemetryEvent.newBuilder(UiAction.CONTENT_LIMIT_QUERY)
+                  .setCarAppContentLimitQuery(
+                      ContentLimitQuery.getContentLimitQuery(contentType, contentValue)));
+      return contentValue;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.java
new file mode 100644
index 0000000..666b5d6
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/UIController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template;
+
+import android.content.ComponentName;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.view.SurfaceProvider;
+
+/**
+ * Implements the UI operations that a client app may trigger in the UI.
+ *
+ * <p>This is normally implemented by a backing context such as an activity or fragment that a given
+ * template host is connected to.
+ *
+ * <p>The methods in this interface are tagged with an {@code appName} parameter that indicates
+ * which application the operation is intended to. The controller must drop the call if it is not
+ * currently connected to that app.
+ *
+ * <p>These methods can also drop the calls, or return {@code null} if the backing context is not
+ * available, e.g. because it's been collected by the GC or explicitly cleared.
+ */
+public interface UIController {
+  /** Sets the {@link TemplateWrapper} to display in the given app's view. */
+  void setTemplate(ComponentName appName, TemplateWrapper template);
+
+  /** Returns the {@link SurfaceProvider} for the given app. */
+  SurfaceProvider getSurfaceProvider(ComponentName appName);
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java
new file mode 100644
index 0000000..53bf270
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionStripWrapper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template.view.model;
+
+import androidx.car.app.model.Action;
+import androidx.car.app.model.ActionStrip;
+import com.android.car.libraries.apphost.template.view.model.ActionWrapper.OnClickListener;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A host side wrapper for {@link ActionStrip} to allow additional callbacks to the host. */
+public final class ActionStripWrapper {
+  /**
+   * An invalid focused action index.
+   *
+   * <p>If this value is set, the focus will remain at the user's last focused button.
+   */
+  public static final int INVALID_FOCUSED_ACTION_INDEX = -1;
+
+  private final List<ActionWrapper> mActions;
+  private int mFocusedActionIndex;
+
+  /**
+   * Instantiates an {@link ActionStripWrapper}.
+   *
+   * <p>The optional {@link OnClickListener} allows the host to be notified when an action is
+   * clicked.
+   */
+  public ActionStripWrapper(List<ActionWrapper> actionWrappers, int focusedActionIndex) {
+    this.mActions = actionWrappers;
+    this.mFocusedActionIndex = focusedActionIndex;
+  }
+
+  /** Returns the list of {@link ActionWrapper} in the action strip. */
+  public List<ActionWrapper> getActions() {
+    return mActions;
+  }
+
+  /**
+   * Returns the focused action index determined by the host.
+   *
+   * <p>The value of {@link #INVALID_FOCUSED_ACTION_INDEX} means that the host did not specify any
+   * action button to focus, in which case the focus will remain at the user's last focused button.
+   */
+  public int getFocusedActionIndex() {
+    return mFocusedActionIndex;
+  }
+
+  /** The builder of {@link ActionStripWrapper}. */
+  public static final class Builder {
+    private final List<ActionWrapper> mActions;
+    private int mFocusedActionIndex = INVALID_FOCUSED_ACTION_INDEX;
+
+    /** Creates an {@link Builder} instance with the given list of {@link ActionWrapper}s. */
+    public Builder(List<ActionWrapper> actions) {
+      this.mActions = actions;
+    }
+
+    /** Creates an {@link Builder} instance with the given {@link ActionStrip}. */
+    public Builder(ActionStrip actionStrip) {
+      List<ActionWrapper> actions = new ArrayList<>();
+      for (Action action : actionStrip.getActions()) {
+        actions.add(new ActionWrapper.Builder(action).build());
+      }
+      this.mActions = actions;
+    }
+
+    /** Sets the index of the action button to focus. */
+    public Builder setFocusedActionIndex(int index) {
+      this.mFocusedActionIndex = index;
+      return this;
+    }
+
+    /** Constructs an {@link ActionStripWrapper} instance defined by this builder. */
+    public ActionStripWrapper build() {
+      if (mFocusedActionIndex < 0 || mFocusedActionIndex >= mActions.size()) {
+        mFocusedActionIndex = INVALID_FOCUSED_ACTION_INDEX;
+      }
+      return new ActionStripWrapper(mActions, mFocusedActionIndex);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java
new file mode 100644
index 0000000..282fde2
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/ActionWrapper.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template.view.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.Action;
+
+/** A host side wrapper for {@link Action} to allow additional callbacks to the host. */
+public class ActionWrapper {
+  /** A host-side on-click listener. */
+  public interface OnClickListener {
+    /** Called when the user clicks the action. */
+    void onClick();
+  }
+
+  private final Action mAction;
+  @Nullable private final OnClickListener mOnClickListener;
+
+  /** Returns the wrapped action. */
+  @NonNull
+  public Action get() {
+    return mAction;
+  }
+
+  /** Returns the host-side on-click listener. */
+  @Nullable
+  public OnClickListener getOnClickListener() {
+    return mOnClickListener;
+  }
+
+  /** Instantiates an {@link ActionWrapper}. */
+  private ActionWrapper(Action action, @Nullable OnClickListener onClickListener) {
+    this.mAction = action;
+    this.mOnClickListener = onClickListener;
+  }
+
+  /** The builder of {@link ActionWrapper}. */
+  public static final class Builder {
+    private final Action mAction;
+    @Nullable private OnClickListener mOnClickListener;
+
+    /** Creates an {@link Builder} instance with the given {@link Action}. */
+    public Builder(Action action) {
+      this.mAction = action;
+    }
+
+    /** Sets the host-side {@link OnClickListener}. */
+    public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
+      this.mOnClickListener = onClickListener;
+      return this;
+    }
+
+    /** Constructs an {@link ActionWrapper} instance defined by this builder. */
+    public ActionWrapper build() {
+      return new ActionWrapper(mAction, mOnClickListener);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java
new file mode 100644
index 0000000..294dc6d
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapper.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template.view.model;
+
+import static com.android.car.libraries.apphost.template.view.model.RowWrapper.ROW_FLAG_NONE;
+
+import android.content.Context;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarColor;
+import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarLocation;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.Metadata;
+import androidx.car.app.model.OnItemVisibilityChangedDelegate;
+import androidx.car.app.model.OnSelectedDelegate;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.PlaceMarker;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.SectionedItemList;
+import androidx.car.app.model.Toggle;
+import com.android.car.libraries.apphost.distraction.constraints.RowConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.template.view.model.RowWrapper.RowFlags;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A host side wrapper for both {@link ItemList} and {@link Pane} to allow additional metadata such
+ * as a {@link androidx.car.app.model.Place} for each individual row and/or {@link
+ * RowListConstraints}.
+ */
+public class RowListWrapper {
+  /** Represents different flags to determine how to render the list. */
+  // TODO(b/174601019): clean this up along with RowFlags
+  @IntDef(
+      flag = true,
+      value = {
+        LIST_FLAGS_NONE,
+        LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS,
+        LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW,
+        LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW,
+        LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW,
+        LIST_FLAGS_RENDER_TITLE_AS_SECONDARY,
+        LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE,
+      })
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface ListFlags {}
+
+  public static final int LIST_FLAGS_NONE = (1 << 0);
+
+  /** The list is selectable, and selection should be rendered with radio buttons. */
+  public static final int LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS = (1 << 1);
+
+  /** The list is selectable, and selection should be rendered by highlighting the row. */
+  public static final int LIST_FLAGS_SELECTABLE_HIGHLIGHT_ROW = (1 << 2);
+
+  /** The list is selectable, and focus on a row would select it. */
+  public static final int LIST_FLAGS_SELECTABLE_FOCUS_SELECT_ROW = (1 << 3);
+
+  /** The list is selectable, and selection will scroll the list to the selected row. */
+  public static final int LIST_FLAGS_SELECTABLE_SCROLL_TO_ROW = (1 << 4);
+
+  /** Renders the title of the rows as secondary text. */
+  public static final int LIST_FLAGS_RENDER_TITLE_AS_SECONDARY = (1 << 5);
+
+  /** Whether the list is placed alongside an image that needs to scroll with the list. */
+  public static final int LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE = (1 << 6);
+
+  /** Whether the list should hide the dividers between the rows. */
+  public static final int LIST_FLAGS_HIDE_ROW_DIVIDERS = (1 << 7);
+
+  /** The default flags to use for selectable lists. */
+  private static final int DEFAULT_SELECTABLE_LIST_FLAGS = LIST_FLAGS_SELECTABLE_USE_RADIO_BUTTONS;
+
+  private final boolean mIsLoading;
+  private final boolean mIsRefresh;
+  @Nullable private final List<Object> mRowList;
+  @Nullable private final CarText mEmptyListText;
+  @Nullable private final CarIcon mImage;
+  @Nullable private final OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate;
+  @Nullable private final Runnable mOnRepeatedSelectionCallback;
+  private final List<RowWrapper> mRowWrappers;
+  private final RowListConstraints mRowListConstraints;
+  @ListFlags private final int mListFlags;
+  private final boolean mIsHalfList;
+
+  /** Returns a builder of {@link RowListWrapper} that wraps the given {@link ItemList}. */
+  public static Builder wrap(Context context, @Nullable ItemList itemList) {
+    if (itemList == null) {
+      return new Builder(context);
+    }
+
+    @SuppressWarnings("unchecked")
+    List<Object> rows = (List) itemList.getItems();
+    Builder builder =
+        new Builder(context)
+            .setRows(rows)
+
+            // Set the default flags for the list, which can be overridden by the caller
+            // to the builder.
+            .setListFlags(getDefaultListFlags(itemList))
+            .setEmptyListText(itemList.getNoItemsMessage())
+            .setOnItemVisibilityChangedDelegate(itemList.getOnItemVisibilityChangedDelegate());
+
+    OnSelectedDelegate onSelectedDelegate = itemList.getOnSelectedDelegate();
+    if (onSelectedDelegate != null) {
+      // Create a selection group for the rows that encompasses the entire list.
+      // The selection groups keep a mutable selection index, and allow for having multiple
+      // selection groups (e.g. different sections of radio buttons) within the same list.
+      builder.setSelectionGroup(
+          SelectionGroup.create(
+              0, rows.size() - 1, itemList.getSelectedIndex(), onSelectedDelegate));
+    }
+
+    return builder;
+  }
+
+  /** Returns a builder of {@link RowListWrapper} that wraps the given {@link SectionedItemList}. */
+  public static Builder wrap(Context context, List<SectionedItemList> sectionLists) {
+    if (sectionLists.isEmpty()) {
+      return new Builder(context);
+    }
+
+    @SuppressWarnings("unchecked")
+    List<Object> rows = (List) sectionLists;
+    return new Builder(context).setRows(rows);
+  }
+
+  /** Returns a builder of {@link RowListWrapper} that wraps the given {@link Pane}. */
+  public static Builder wrap(Context context, @Nullable Pane pane) {
+    if (pane == null) {
+      L.w(LogTags.TEMPLATE, "Pane is expected on the template but not set");
+      return new Builder(context);
+    }
+
+    // TODO(b/205522074): large image and dividers are specific to pane and the UI hierarchy between
+    // list and pane is diverging more and more. Investigate whether we can decouple the two.
+    int flags = LIST_FLAGS_HIDE_ROW_DIVIDERS;
+    if (pane.getImage() != null) {
+      flags |= LIST_FLAGS_TEMPLATE_HAS_LARGE_IMAGE;
+    }
+
+    return new Builder(context)
+        .setRows(new ArrayList<>(pane.getRows()))
+        .setListFlags(flags)
+        .setImage(pane.getImage())
+        .setIsLoading(pane.isLoading());
+  }
+
+  /** Returns the list flags to use by default for the given {@link ItemList}. */
+  @ListFlags
+  public static int getDefaultListFlags(@Nullable ItemList itemList) {
+    return itemList != null && itemList.getOnSelectedDelegate() != null
+        ? DEFAULT_SELECTABLE_LIST_FLAGS
+        : LIST_FLAGS_NONE;
+  }
+
+  /** Returns a builder configured with the values from this {@link RowListWrapper} instance. */
+  public Builder newBuilder(Context context) {
+    return new Builder(context, this);
+  }
+
+  /**
+   * Returns the list of rows that make up this list.
+   *
+   * @see Builder#setRows(List)
+   */
+  @Nullable
+  public List<Object> getRows() {
+    return mRowList == null ? null : ImmutableList.copyOf(mRowList);
+  }
+
+  /** Returns the image that should be shown alongside the row list. */
+  @Nullable
+  public CarIcon getImage() {
+    return mImage;
+  }
+
+  /**
+   * Returns the flags that control how to render the list.
+   *
+   * @see Builder#setListFlags(int)
+   */
+  @ListFlags
+  public int getListFlags() {
+    return mListFlags;
+  }
+
+  /**
+   * Returns the delegate to use to notify when when the visibility of items in the list change, for
+   * example during scroll.
+   *
+   * @see Builder#setOnItemVisibilityChangedDelegate(OnItemVisibilityChangedDelegate)
+   */
+  @Nullable
+  public OnItemVisibilityChangedDelegate getOnItemVisibilityChangedDelegate() {
+    return mOnItemVisibilityChangedDelegate;
+  }
+
+  /**
+   * Returns the callback for when a row is repeatedly selected.
+   *
+   * @see Builder#setOnRepeatedSelectionCallback
+   */
+  @Nullable
+  public Runnable getRepeatedSelectionCallback() {
+    return mOnRepeatedSelectionCallback;
+  }
+
+  /** Returns whether the list has no rows. */
+  public boolean isEmpty() {
+    return mRowWrappers.isEmpty();
+  }
+
+  /**
+   * Returns the {@link RowListConstraints} that define the restrictions to apply to the list.
+   *
+   * @see Builder#setRowListConstraints(RowListConstraints)
+   */
+  public RowListConstraints getRowListConstraints() {
+    return mRowListConstraints;
+  }
+
+  /**
+   * Returns the text to display when the list is empty or {@code null} to not display any text.
+   *
+   * @see Builder#setEmptyListText(CarText)
+   */
+  @Nullable
+  public CarText getEmptyListText() {
+    return mEmptyListText;
+  }
+
+  /** Returns the list of {@link RowWrapper} instances that wrap the rows in the list. */
+  public List<RowWrapper> getRowWrappers() {
+    return mRowWrappers;
+  }
+
+  /**
+   * Returns whether the list is in loading state.
+   *
+   * @see Builder#setIsLoading(boolean)
+   */
+  public boolean isLoading() {
+    return mIsLoading;
+  }
+
+  /**
+   * Returns whether the list is in loading state.
+   *
+   * @see Builder#setIsRefresh(boolean)
+   */
+  public boolean isRefresh() {
+    return mIsRefresh;
+  }
+
+  /**
+   * Returns whether this is a half list, as opposed to a full width list.
+   *
+   * @see Builder#setIsHalfList(boolean)
+   */
+  public boolean isHalfList() {
+    return mIsHalfList;
+  }
+
+  /**
+   * Builds the {@link RowWrapper}s for a given list, expanding any sub-lists embedded within it.
+   */
+  @SuppressWarnings("RestrictTo")
+  private static ImmutableList<RowWrapper> buildRowWrappers(
+      Context context,
+      @Nullable List<Object> rowList,
+      @Nullable SelectionGroup selectionGroup,
+      RowListConstraints rowListConstraints,
+      @Nullable CarText selectedText,
+      @RowFlags int rowFlags,
+      @ListFlags int listFlags,
+      int startIndex,
+      boolean isHalfList) {
+    if (rowList == null || rowList.isEmpty()) {
+      return ImmutableList.of();
+    }
+
+    // If selectable lists are disallowed, set the selection group to null, which effectively
+    // disables selection.
+    if (!rowListConstraints.getAllowSelectableLists()) {
+      L.w(LogTags.TEMPLATE, "Selectable lists disallowed for template this list");
+      selectionGroup = null;
+    }
+
+    int labelIndex = 1;
+    ImmutableList.Builder<RowWrapper> wrapperListBuilder = new ImmutableList.Builder<>();
+
+    // Sub-lists are expanded inline in this list and become part of it. This size is the
+    // number of rows accounting for any such sub-list expansions.
+    int expandedSize = 0;
+
+    for (Object rowObj : rowList) {
+      // The row is a sub-list: we will expand it and add its rows to the parent list.
+      if (rowObj instanceof SectionedItemList) {
+        SectionedItemList section = (SectionedItemList) rowObj;
+        ItemList subList = section.getItemList();
+
+        if (subList == null || subList.getItems().isEmpty()) {
+          // This should never happen as the client side should prevent empty sub-lists.
+          L.e(LogTags.TEMPLATE, "Found empty sub-list, skipping...");
+          continue;
+        }
+
+        CarText header = section.getHeader();
+        if (header == null) {
+          // This should never happen as the client side should prevent null headers.
+          L.e(LogTags.TEMPLATE, "Header is expected on the section but not set, skipping...");
+          continue;
+        }
+
+        // Create a row representing the header.
+        Row headerRow = new Row.Builder().setTitle(header.toCharSequence()).build();
+        wrapperListBuilder.add(
+            RowWrapper.wrap(headerRow, startIndex + expandedSize)
+                .setListFlags(listFlags)
+                .setRowFlags(rowFlags | RowWrapper.ROW_FLAG_SECTION_HEADER)
+                .setIsHalfList(isHalfList)
+                .setRowConstraints(rowListConstraints.getRowConstraints())
+                .build());
+        expandedSize++;
+
+        // Create wrappers for each row in the sublist.
+        int subListSize = subList.getItems().size();
+        List<RowWrapper> subWrappers =
+            createRowWrappersForSublist(
+                context,
+                subList,
+                expandedSize,
+                rowListConstraints,
+                selectedText,
+                rowFlags,
+                listFlags,
+                isHalfList);
+        wrapperListBuilder.addAll(subWrappers);
+        expandedSize += subListSize;
+      } else {
+        RowWrapper.Builder wrapperBuilder = RowWrapper.wrap(rowObj, startIndex + expandedSize);
+        RowConstraints rowConstraints = rowListConstraints.getRowConstraints();
+        if (rowObj instanceof Row) {
+          Row row = (Row) rowObj;
+          labelIndex = addMetadataToRowWrapper(row, wrapperBuilder, labelIndex);
+
+          Toggle toggle = row.getToggle();
+          if (toggle != null) {
+            wrapperBuilder.setIsToggleChecked(toggle.isChecked());
+          }
+
+          wrapperBuilder.setSelectedText(selectedText);
+        }
+
+        wrapperListBuilder.add(
+            wrapperBuilder
+                .setRowFlags(rowFlags)
+                .setListFlags(listFlags)
+                .setIsHalfList(isHalfList)
+                .setSelectionGroup(selectionGroup)
+                .setRowConstraints(rowConstraints)
+                .build());
+
+        expandedSize++;
+      }
+    }
+
+    return wrapperListBuilder.build();
+  }
+
+  /**
+   * Adds any metadata from the original {@link Row} to its {@link RowWrapper}.
+   *
+   * <p>If a {@link Row} contains a default marker, this updates the marker to render a string based
+   * on the given {@code labelIndex}.
+   *
+   * @return the updated label index value that should be used for the next default marker
+   */
+  private static int addMetadataToRowWrapper(
+      Row row, RowWrapper.Builder wrapperBuilder, int labelIndex) {
+    Metadata metadata = row.getMetadata();
+    if (metadata != null) {
+      Place place = metadata.getPlace();
+      if (place != null) {
+        CarLocation location = place.getLocation();
+        if (location != null) {
+          // Assign any default markers (without text/icon) to show an integer value.
+          PlaceMarker marker = place.getMarker();
+          if (isDefaultMarker(marker)) {
+            PlaceMarker.Builder markerBuilder =
+                new PlaceMarker.Builder().setLabel(Integer.toString(labelIndex));
+            if (marker != null) {
+              CarColor markerColor = marker.getColor();
+              if (markerColor != null) {
+                markerBuilder.setColor(markerColor);
+              }
+              place = new Place.Builder(location).setMarker(markerBuilder.build()).build();
+              metadata = new Metadata.Builder(metadata).setPlace(place).build();
+            }
+            labelIndex++;
+          }
+        }
+      }
+
+      // Sets the metadata in the wrapper with the updated marker if set.
+      wrapperBuilder.setMetadata(metadata);
+    }
+
+    return labelIndex;
+  }
+
+  private static boolean isDefaultMarker(@Nullable PlaceMarker marker) {
+    return marker != null && marker.getIcon() == null && marker.getLabel() == null;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static ImmutableList<RowWrapper> createRowWrappersForSublist(
+      Context context,
+      ItemList subList,
+      int currentIndex,
+      RowListConstraints rowListConstraints,
+      @Nullable CarText selectedText,
+      @RowFlags int rowFlags,
+      @ListFlags int listFlags,
+      boolean isHalfList) {
+    // Create a selection group for this sub-list.
+    // Offset its indices it by the expanded size to account for any previously expanded
+    // sub-lists.
+    OnSelectedDelegate onSelectedDelegate = subList.getOnSelectedDelegate();
+    SelectionGroup subSelectionGroup =
+        onSelectedDelegate != null
+            ? SelectionGroup.create(
+                currentIndex,
+                currentIndex + subList.getItems().size() - 1,
+                currentIndex + subList.getSelectedIndex(),
+                onSelectedDelegate)
+            : null;
+
+    return buildRowWrappers(
+        context,
+        (List) subList.getItems(),
+        subSelectionGroup,
+        rowListConstraints,
+        selectedText,
+        rowFlags,
+        listFlags == 0 ? getDefaultListFlags(subList) : listFlags,
+        /* startIndex = */ currentIndex,
+        isHalfList);
+  }
+
+  private RowListWrapper(Builder builder) {
+    mIsLoading = builder.mIsLoading;
+    mRowList = builder.mRowList;
+    mImage = builder.mImage;
+    mEmptyListText = builder.mEmptyListText;
+    mOnRepeatedSelectionCallback = builder.mOnRepeatedSelectionCallback;
+    mOnItemVisibilityChangedDelegate = builder.mOnItemVisibilityChangedDelegate;
+    mRowListConstraints = builder.mRowListConstraints;
+    mListFlags = builder.mListFlags;
+    mIsRefresh = builder.mIsRefresh;
+    mIsHalfList = builder.mIsHalfList;
+    mRowWrappers =
+        buildRowWrappers(
+            builder.mContext,
+            builder.mRowList,
+            builder.mSelectionGroup,
+            builder.mRowListConstraints,
+            builder.mSelectedText,
+            builder.mRowFlags,
+            builder.mListFlags,
+            /* startIndex = */ 0,
+            builder.mIsHalfList);
+  }
+
+  /** The builder class for {@link RowListWrapper}. */
+  public static class Builder {
+    private final Context mContext;
+    @Nullable SelectionGroup mSelectionGroup;
+    @Nullable private List<Object> mRowList;
+    @RowFlags private int mRowFlags = ROW_FLAG_NONE;
+    @ListFlags private int mListFlags;
+    private RowListConstraints mRowListConstraints =
+        RowListConstraints.ROW_LIST_CONSTRAINTS_CONSERVATIVE;
+    @Nullable private Runnable mOnRepeatedSelectionCallback;
+    @Nullable private OnItemVisibilityChangedDelegate mOnItemVisibilityChangedDelegate;
+    private boolean mIsLoading;
+    private boolean mIsRefresh;
+    @Nullable private CarText mEmptyListText;
+    @Nullable private CarText mSelectedText;
+    @Nullable private CarIcon mImage;
+    private boolean mIsHalfList;
+
+    private Builder(Context context) {
+      mContext = context;
+      mRowList = null;
+    }
+
+    private Builder(Context context, RowListWrapper rowListWrapper) {
+      mContext = context;
+      mRowList = rowListWrapper.mRowList;
+      mImage = rowListWrapper.mImage;
+      mListFlags = rowListWrapper.mListFlags;
+      mRowListConstraints = rowListWrapper.mRowListConstraints;
+      mIsLoading = rowListWrapper.mIsLoading;
+      mIsRefresh = rowListWrapper.mIsRefresh;
+      mIsHalfList = rowListWrapper.mIsHalfList;
+      mEmptyListText = rowListWrapper.mEmptyListText;
+      mOnItemVisibilityChangedDelegate = rowListWrapper.mOnItemVisibilityChangedDelegate;
+      mOnRepeatedSelectionCallback = rowListWrapper.mOnRepeatedSelectionCallback;
+    }
+
+    /** Sets the set of rows that make up the list. */
+    public Builder setRows(@Nullable List<Object> rowList) {
+      mRowList = rowList;
+      return this;
+    }
+
+    /** Sets the image to be shown alongside the rows. */
+    public Builder setImage(@Nullable CarIcon image) {
+      mImage = image;
+      return this;
+    }
+
+    /** Set an extra callback for when a row in the list has been repeatedly selected. */
+    public Builder setOnRepeatedSelectionCallback(@Nullable Runnable runnable) {
+      mOnRepeatedSelectionCallback = runnable;
+      return this;
+    }
+
+    /**
+     * Sets the delegate to use to notify when when the visibility of items in the list change, for
+     * example, during scroll.
+     */
+    public Builder setOnItemVisibilityChangedDelegate(
+        @Nullable OnItemVisibilityChangedDelegate delegate) {
+      mOnItemVisibilityChangedDelegate = delegate;
+      return this;
+    }
+
+    /**
+     * Sets whether the list is loading.
+     *
+     * <p>If set to {@code true}, the UI shows a loading indicator and ignore any rows added to the
+     * list. If set to {@code false}, the UI shows the actual row contents.
+     */
+    public Builder setIsLoading(boolean isLoading) {
+      mIsLoading = isLoading;
+      return this;
+    }
+
+    /**
+     * Sets whether the list is a refresh of the existing list.
+     *
+     * <p>If set to {@code true}, the UI will not scroll to top, otherwise it will.
+     */
+    public Builder setIsRefresh(boolean isRefresh) {
+      mIsRefresh = isRefresh;
+      return this;
+    }
+
+    /** Sets the text to add to a row when it is selected. */
+    public Builder setRowSelectedText(@Nullable CarText selectedText) {
+      mSelectedText = selectedText;
+      return this;
+    }
+
+    /** Sets the text to display when the list is empty or {@code null} to not display any text. */
+    public Builder setEmptyListText(@Nullable CarText emptyListText) {
+      mEmptyListText = emptyListText;
+      return this;
+    }
+
+    /**
+     * Sets a selection group for this list.
+     *
+     * <p>Selection groups are used for defining a mutually-exclusive selectable range of rows,
+     * which can be used for example for radio buttons.
+     *
+     * @see SelectionGroup
+     */
+    public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) {
+      mSelectionGroup = selectionGroup;
+      return this;
+    }
+
+    /**
+     * Set whether the list is a "half" list.
+     *
+     * <p>"Half list " is the term we use for lists that don't span the entire width of the screen
+     * (e.g. inside of a card in a map template). Note these don't necessarily take exactly half the
+     * width (depending on the screen width and how the card may adapt to it).
+     */
+    public Builder setIsHalfList(boolean isHalfList) {
+      mIsHalfList = isHalfList;
+      return this;
+    }
+
+    /** Sets the flags that control how to render individual rows. */
+    public Builder setRowFlags(@RowFlags int rowFlags) {
+      mRowFlags = rowFlags;
+      return this;
+    }
+
+    /** Sets the flags that control how to render the list. */
+    public Builder setListFlags(@ListFlags int listFlags) {
+      mListFlags = listFlags;
+      return this;
+    }
+
+    /** Sets the {@link RowListConstraints} that define the restrictions to apply to the list. */
+    public Builder setRowListConstraints(RowListConstraints constraints) {
+      mRowListConstraints = constraints;
+      return this;
+    }
+
+    /** Constructs a {@link RowListWrapper} instance from this builder. */
+    public RowListWrapper build() {
+      return new RowListWrapper(this);
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java
new file mode 100644
index 0000000..d2793e3
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowListWrapperTemplate.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template.view.model;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+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.ListTemplate;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.Template;
+import com.android.car.libraries.apphost.distraction.constraints.ActionsConstraints;
+import com.android.car.libraries.apphost.distraction.constraints.RowListConstraints;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/**
+ * A template that wraps {@link RowListWrapper}-based templates.
+ *
+ * <p>This template is used to to render full-screen homogeneous lists, or panes (which are also
+ * built with lists).
+ *
+ * @see #wrap
+ */
+public class RowListWrapperTemplate implements Template {
+  private final RowListWrapper mList;
+  @Nullable private final CarText mTitle;
+  @Nullable private final Action mHeaderAction;
+  @Nullable private final ActionStrip mActionStrip;
+  @Nullable private final List<Action> mActionList;
+  private final ActionsConstraints mActionsconstraints;
+
+  /** The original template being wrapped. */
+  private final Template mTemplate;
+
+  /** Returns the list used by the template. */
+  public RowListWrapper getList() {
+    return mList;
+  }
+
+  /** Returns the title of the template. */
+  @Nullable
+  public CarText getTitle() {
+    return mTitle;
+  }
+
+  /**
+   * Returns the {@link Action} to display in the template's header or {@code null} if one is not to
+   * be displayed.
+   */
+  @Nullable
+  public Action getHeaderAction() {
+    return mHeaderAction;
+  }
+
+  /**
+   * Returns the {@link ActionStrip} to display in the template or {@code null} if one is not to be
+   * displayed.
+   */
+  @Nullable
+  public ActionStrip getActionStrip() {
+    return mActionStrip;
+  }
+
+  /**
+   * Returns the list of {@link Action}s to display in the template or {@code null} if one is not to
+   * be displayed.
+   */
+  @Nullable
+  public List<Action> getActionList() {
+    return mActionList;
+  }
+
+  /**
+   * Returns the constraints for the actions in the template.
+   *
+   * @see ActionsConstraints
+   */
+  public ActionsConstraints getActionsConstraints() {
+    return mActionsconstraints;
+  }
+
+  @NonNull
+  @Override
+  public String toString() {
+    return "RowListWrapperTemplate(" + mTemplate + ")";
+  }
+
+  /**
+   * Returns a {@link RowListWrapperTemplate} instance that wraps the given {@code template}.
+   *
+   * @throws IllegalArgumentException if the {@code template} is not of a type that can be wrapped
+   */
+  public static RowListWrapperTemplate wrap(Context context, Template template, boolean isRefresh) {
+    if (template instanceof PaneTemplate) {
+      PaneTemplate paneTemplate = (PaneTemplate) template;
+      Pane pane = paneTemplate.getPane();
+      return new RowListWrapperTemplate(
+          template,
+          RowListWrapper.wrap(context, pane)
+              .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_PANE)
+              .setIsRefresh(isRefresh)
+              .build(),
+          paneTemplate.getTitle(),
+          paneTemplate.getHeaderAction(),
+          paneTemplate.getActionStrip(),
+          paneTemplate.getPane().getActions(),
+          ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+    } else if (template instanceof ListTemplate) {
+      ListTemplate listTemplate = (ListTemplate) template;
+      RowListWrapper.Builder listWrapperBuilder;
+      if (listTemplate.isLoading()) {
+        listWrapperBuilder =
+            RowListWrapper.wrap(context, ImmutableList.of())
+                .setIsLoading(true)
+                .setIsRefresh(isRefresh);
+      } else {
+        ItemList singleList = listTemplate.getSingleList();
+        listWrapperBuilder =
+            singleList == null
+                ? RowListWrapper.wrap(context, listTemplate.getSectionedLists())
+                    .setIsRefresh(isRefresh)
+                : RowListWrapper.wrap(context, singleList).setIsRefresh(isRefresh);
+      }
+
+      return new RowListWrapperTemplate(
+          template,
+          listWrapperBuilder
+              .setRowListConstraints(RowListConstraints.ROW_LIST_CONSTRAINTS_FULL_LIST)
+              .setRowFlags(RowWrapper.DEFAULT_UNIFORM_LIST_ROW_FLAGS)
+              .build(),
+          listTemplate.getTitle(),
+          listTemplate.getHeaderAction(),
+          listTemplate.getActionStrip(),
+          /* actionList= */ null,
+          ActionsConstraints.ACTIONS_CONSTRAINTS_SIMPLE);
+    } else {
+      throw new IllegalArgumentException(
+          "Unknown template class: " + template.getClass().getName());
+    }
+  }
+
+  /** Returns the template wrapped by this instance of a {@link RowListWrapperTemplate}. */
+  @VisibleForTesting
+  public Template getTemplate() {
+    return mTemplate;
+  }
+
+  private RowListWrapperTemplate(
+      Template template,
+      RowListWrapper list,
+      @Nullable CarText title,
+      @Nullable Action headerAction,
+      @Nullable ActionStrip actionStrip,
+      @Nullable List<Action> actionList,
+      ActionsConstraints actionsConstraints) {
+    mTemplate = template;
+    mList = list;
+    mTitle = title;
+    mHeaderAction = headerAction;
+    mActionStrip = actionStrip;
+    mActionList = actionList;
+    mActionsconstraints = actionsConstraints;
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java
new file mode 100644
index 0000000..decaf66
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/RowWrapper.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template.view.model;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.Metadata;
+import androidx.car.app.model.Place;
+import androidx.car.app.model.Row;
+import com.android.car.libraries.apphost.distraction.constraints.RowConstraints;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A host side wrapper for {@link Row} which can include extra metadata such as a {@link Place}. */
+public class RowWrapper {
+  /** Represents flags that control some attributes of the row. */
+  // TODO(b/174601019): clean this up along with ListFlags
+  @IntDef(
+      value = {ROW_FLAG_NONE, ROW_FLAG_SHOW_DIVIDERS, ROW_FLAG_SECTION_HEADER},
+      flag = true)
+  @Retention(RetentionPolicy.SOURCE)
+  public @interface RowFlags {}
+
+  /** 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);
+
+  /** The default flags to use for uniform lists. */
+  public static final int DEFAULT_UNIFORM_LIST_ROW_FLAGS = ROW_FLAG_SHOW_DIVIDERS;
+
+  private final Object mRow;
+  private final int mRowIndex;
+  private final Metadata mMetadata;
+  @Nullable private final CarText mSelectedText;
+  @RowFlags private final int mRowFlags;
+  @RowListWrapper.ListFlags private final int mListFlags;
+  private final RowConstraints mRowConstraints;
+  private boolean mIsHalfList;
+
+  /**
+   * The selection group this row belongs to, or {@code null} if the row does not belong to one.
+   *
+   * <p>Selection groups are used to establish mutually-exclusive scopes of row selection, for
+   * example, to implement radio button groups.
+   *
+   * <p>The selection index in the group is mutable and allows for automatically updating it the UI
+   * at the host without a round-trip to the client to change the selection there.
+   */
+  @Nullable private final SelectionGroup mSelectionGroup;
+
+  /**
+   * Whether the toggle is checked.
+   *
+   * <p>This field is mutable so that we can remember toggle changes on the host without having to
+   * round-trip to the client when toggle states change.
+   *
+   * <p>It is initialized with the initial value from the model coming from the client, then can
+   * mutate after.
+   */
+  private boolean mIsToggleChecked;
+
+  /** Returns a {@link Builder} that wraps a row with the provided index. */
+  public static Builder wrap(Object row, int rowIndex) {
+    return new Builder(row, rowIndex);
+  }
+
+  @Override
+  public String toString() {
+    return "[" + mRow + ", group: " + mSelectionGroup + "]";
+  }
+
+  /** Returns the actual {@link Row} object that this instance is wrapping. */
+  public Object getRow() {
+    return mRow;
+  }
+
+  /** Returns the absolute index of the row in the flattened container list. */
+  public int getRowIndex() {
+    return mRowIndex;
+  }
+
+  /** Returns the {@link Metadata} that is associated with the row. */
+  public Metadata getMetadata() {
+    return mMetadata;
+  }
+
+  /** Returns the {@link CarText} that should be displayed in the row when it has focus. */
+  @Nullable
+  public CarText getSelectedText() {
+    return mSelectedText;
+  }
+
+  /**
+   * Returns the flags that control how to render this row.
+   *
+   * @see Builder#setRowFlags(int)
+   */
+  @RowFlags
+  public int getRowFlags() {
+    return mRowFlags;
+  }
+
+  /**
+   * Returns the flags that control how to render the list this row belongs to.
+   *
+   * @see Builder#setListFlags
+   */
+  @RowListWrapper.ListFlags
+  public int getListFlags() {
+    return mListFlags;
+  }
+
+  /**
+   * Returns whether the row belongs to a "half" list.
+   *
+   * @see Builder#setIsHalfList(boolean)
+   */
+  public boolean isHalfList() {
+    return mIsHalfList;
+  }
+
+  /**
+   * Returns the selection group this row belongs to.
+   *
+   * @see Builder#setSelectionGroup(SelectionGroup)
+   */
+  @Nullable
+  public SelectionGroup getSelectionGroup() {
+    return mSelectionGroup;
+  }
+
+  /**
+   * Returns whether the toggle in the row, if there is one, is checked.
+   *
+   * @see Builder#setIsToggleChecked(boolean)
+   */
+  public boolean isToggleChecked() {
+    return mIsToggleChecked;
+  }
+
+  /** Checks the toggle in the row if unchecked, and vice-versa. */
+  public void switchToggleState() {
+    mIsToggleChecked = !mIsToggleChecked;
+  }
+
+  /**
+   * Returns the {@link RowConstraints} that define the restrictions to apply to the row.
+   *
+   * @see Builder#setRowConstraints(RowConstraints)
+   */
+  public RowConstraints getRowConstraints() {
+    return mRowConstraints;
+  }
+
+  private RowWrapper(Builder builder) {
+    mRow = builder.mRow;
+    mRowIndex = builder.mRowIndex;
+    mMetadata = builder.mEmptyMetadata;
+    mSelectedText = builder.mSelectedText;
+    mRowFlags = builder.mRowFlags;
+    mListFlags = builder.mListFlags;
+    mSelectionGroup = builder.mSelectionGroup;
+    mIsToggleChecked = builder.mIsToggleChecked;
+    mRowConstraints = builder.mRowConstraints;
+    mIsHalfList = builder.mIsHalfList;
+  }
+
+  /** The builder class for {@link RowWrapper}. */
+  public static class Builder {
+    private final Object mRow;
+    private final int mRowIndex;
+    private Metadata mEmptyMetadata = Metadata.EMPTY_METADATA;
+    @RowListWrapper.ListFlags private int mListFlags;
+    @RowFlags private int mRowFlags = ROW_FLAG_NONE;
+    @Nullable private SelectionGroup mSelectionGroup;
+    private boolean mIsToggleChecked;
+    @Nullable private CarText mSelectedText;
+    private RowConstraints mRowConstraints = RowConstraints.ROW_CONSTRAINTS_CONSERVATIVE;
+    private boolean mIsHalfList;
+
+    /** Sets the {@link Metadata} associated with this row. */
+    public Builder setMetadata(Metadata metadata) {
+      mEmptyMetadata = metadata;
+      return this;
+    }
+
+    /** Sets the text to display in the row when it is selected. */
+    public Builder setSelectedText(@Nullable CarText selectedText) {
+      mSelectedText = selectedText;
+      return this;
+    }
+
+    /** Sets the flags that control how to render this row. */
+    public Builder setRowFlags(@RowFlags int rowFlags) {
+      mRowFlags = rowFlags;
+      return this;
+    }
+
+    /** Sets the flags that control how to render the list this row belongs to. */
+    public Builder setListFlags(@RowListWrapper.ListFlags int listFlags) {
+      mListFlags = listFlags;
+      return this;
+    }
+
+    /**
+     * Set whether the list this row belongs to is a "half" list.
+     *
+     * <p>"Half list " is the term we use for lists that don't span the entire width of the screen
+     * (e.g. inside of a card in a map template). Note these don't necessarily take exactly half the
+     * width (depending on the screen width and how the card may adapt to it).
+     */
+    public Builder setIsHalfList(boolean isHalfList) {
+      mIsHalfList = isHalfList;
+      return this;
+    }
+
+    /** Sets the selection group this row belongs to. */
+    public Builder setSelectionGroup(@Nullable SelectionGroup selectionGroup) {
+      mSelectionGroup = selectionGroup;
+      return this;
+    }
+
+    /** Sets whether the toggle in the row, if there is one, should be displayed checked. */
+    public Builder setIsToggleChecked(boolean isToggleChecked) {
+      mIsToggleChecked = isToggleChecked;
+      return this;
+    }
+
+    /** Sets the {@link RowConstraints} that define the restrictions to apply to the row. */
+    public Builder setRowConstraints(RowConstraints rowConstraints) {
+      mRowConstraints = rowConstraints;
+      return this;
+    }
+
+    /** Constructs a {@link RowWrapper} instance from this builder. */
+    public RowWrapper build() {
+      return new RowWrapper(this);
+    }
+
+    private Builder(Object row, int rowIndex) {
+      mRow = row;
+      mRowIndex = rowIndex;
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.java
new file mode 100644
index 0000000..39f8195
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/template/view/model/SelectionGroup.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.template.view.model;
+
+import androidx.car.app.model.OnSelectedDelegate;
+
+/**
+ * Represents a set of rows inside of a list that describe a mutually-exclusive selection group.
+ *
+ * <p>This can be used to describe multiple radio sub-lists within the same list.
+ */
+public class SelectionGroup {
+  private final int mStartIndex;
+  private final int mEndIndex;
+  private final OnSelectedDelegate mOnSelectedDelegate;
+
+  /**
+   * The currently selected index.
+   *
+   * <p>The selection index in the group is mutable and allows for automatically updating it the UI
+   * at the host without a round-trip to the client to change the selection there.
+   */
+  private int mSelectedIndex;
+
+  /**
+   * Returns an instance of a {@link SelectionGroup}.
+   *
+   * @param startIndex the index where the selection group starts, inclusive
+   * @param endIndex the index where the selection ends, inclusive
+   * @param selectedIndex the index of the item in the selection group to select
+   * @param onSelectedDelegate a delegate to invoke upon selection change events
+   */
+  public static SelectionGroup create(
+      int startIndex, int endIndex, int selectedIndex, OnSelectedDelegate onSelectedDelegate) {
+    return new SelectionGroup(startIndex, endIndex, selectedIndex, onSelectedDelegate);
+  }
+
+  /** Returns whether the item at the given index is selected. */
+  public boolean isSelected(int index) {
+    return index == mSelectedIndex;
+  }
+
+  /** Returns the index of the item that's currently selected in the group. */
+  public int getSelectedIndex() {
+    return mSelectedIndex;
+  }
+
+  /** Returns the index relative to the selection group. */
+  public int getRelativeIndex(int index) {
+    return index - mStartIndex;
+  }
+
+  /** Returns the delegate to invoke upon selection change events. */
+  public OnSelectedDelegate getOnSelectedDelegate() {
+    return mOnSelectedDelegate;
+  }
+
+  /** Sets the index of the item to select in the group. */
+  public void setSelectedIndex(int selectedIndex) {
+    checkSelectedIndexOutOfBounds(mStartIndex, mEndIndex, selectedIndex);
+    mSelectedIndex = selectedIndex;
+  }
+
+  @Override
+  public String toString() {
+    return "[start: " + mStartIndex + ", end: " + mEndIndex + ", selected: " + mSelectedIndex + "]";
+  }
+
+  private SelectionGroup(
+      int startIndex, int endIndex, int selectedIndex, OnSelectedDelegate onSelectedDelegate) {
+    checkSelectedIndexOutOfBounds(startIndex, endIndex, selectedIndex);
+    mStartIndex = startIndex;
+    mEndIndex = endIndex;
+    mSelectedIndex = selectedIndex;
+    mOnSelectedDelegate = onSelectedDelegate;
+  }
+
+  private static void checkSelectedIndexOutOfBounds(
+      int startIndex, int endIndex, int selectedIndex) {
+    if (selectedIndex < startIndex || selectedIndex > endIndex) {
+      throw new IndexOutOfBoundsException(
+          "Selected index "
+              + selectedIndex
+              + " not within bounds of ["
+              + startIndex
+              + ", "
+              + endIndex
+              + "]");
+    }
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java
new file mode 100644
index 0000000..4fdbe4e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractSurfaceTemplatePresenter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.view;
+
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+
+/**
+ * Abstract base class for {@link TemplatePresenter}s which have a {@link
+ * androidx.car.app.SurfaceContainer}.
+ */
+public abstract class AbstractSurfaceTemplatePresenter extends AbstractTemplatePresenter
+    implements PanZoomManager.Delegate {
+  /** The time threshold between touch events for 30fps updates. */
+  private static final long TOUCH_UPDATE_THRESHOLD_MILLIS = 30;
+
+  /** The amount in pixels to pan with a rotary nudge. */
+  private static final float ROTARY_NUDGE_PAN_PIXELS = 50f;
+
+  private final OnGlobalLayoutListener mGlobalLayoutListener =
+      new OnGlobalLayoutListener() {
+        @SuppressWarnings("nullness") // suppress under initialization warning for this
+        @Override
+        public void onGlobalLayout() {
+          if (mShouldUpdateVisibleArea) {
+            AbstractSurfaceTemplatePresenter.this.updateVisibleArea();
+            mShouldUpdateVisibleArea = false;
+          }
+        }
+      };
+
+  /** Gesture manager that handles pan and zoom gestures in map-based template presenters. */
+  private final PanZoomManager mPanZoomManager;
+
+  /**
+   * A boolean flag that indicates whether the visible area should be updated in the next layout
+   * phase.
+   */
+  private boolean mShouldUpdateVisibleArea;
+
+  /**
+   * Constructs a new instance of a {@link AbstractTemplatePresenter} with the given {@link
+   * Template}.
+   */
+  @SuppressWarnings({"nullness:method.invocation", "nullness:assignment", "nullness:argument"})
+  public AbstractSurfaceTemplatePresenter(
+      TemplateContext templateContext,
+      TemplateWrapper templateWrapper,
+      StatusBarState statusBarState) {
+    super(templateContext, templateWrapper, statusBarState);
+
+    mPanZoomManager =
+        new PanZoomManager(
+            templateContext, this, getTouchUpdateThresholdMillis(), getRotaryNudgePanPixels());
+  }
+
+  @Override
+  public void onPause() {
+    getView().getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
+    getView().setOnTouchListener(null);
+    getView().setOnGenericMotionListener(null);
+    super.onPause();
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    getView().getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
+
+    L.d(
+        LogTags.TEMPLATE,
+        "Pan and zoom is %s in %s",
+        isPanAndZoomEnabled() ? "ENABLED" : "DISABLED",
+        getTemplate());
+    if (isPanAndZoomEnabled()) {
+      getView().setOnTouchListener(mPanZoomManager);
+      getView().setOnGenericMotionListener(mPanZoomManager);
+    }
+  }
+
+  @Override
+  public boolean usesSurface() {
+    return true;
+  }
+
+  @Override
+  public boolean isFullScreen() {
+    return false;
+  }
+
+  /** Adjusts the {@code inset} according to the views visible on screen. */
+  public abstract void calculateAdditionalInset(Rect inset);
+
+  @Override
+  public void onPanModeChanged(boolean isInPanMode) {
+    // No-op by default
+  }
+
+  /** Returns whether the pan and zoom feature is enabled. */
+  public boolean isPanAndZoomEnabled() {
+    return false;
+  }
+
+  /** Returns the time threshold in milliseconds for processing touch events. */
+  public long getTouchUpdateThresholdMillis() {
+    return TOUCH_UPDATE_THRESHOLD_MILLIS;
+  }
+
+  /** Returns the amount in pixels to pan with a rotary nudge. */
+  public float getRotaryNudgePanPixels() {
+    return ROTARY_NUDGE_PAN_PIXELS;
+  }
+
+  /** Returns the {@link OnGlobalLayoutListener} instance attached to the view tree. */
+  @VisibleForTesting
+  public OnGlobalLayoutListener getGlobalLayoutListener() {
+    return mGlobalLayoutListener;
+  }
+
+  /** Returns the {@link PanZoomManager} instance associated with this presenter. */
+  protected PanZoomManager getPanZoomManager() {
+    return mPanZoomManager;
+  }
+
+  /** Requests an update to the surface's visible area information. */
+  protected void requestVisibleAreaUpdate() {
+    // Flip the flag so that we will update the visible area in our next layout phase. We cannot
+    // just update the visible area here because the views are not laid out when they are just
+    // inflated, which means that we cannot use the view coordinates to calculate where the views
+    // are not drawn.
+    mShouldUpdateVisibleArea = true;
+  }
+
+  private void updateVisibleArea() {
+    View rootView = getView();
+    Rect safeAreaInset = new Rect();
+    safeAreaInset.left = rootView.getLeft() + rootView.getPaddingLeft();
+    safeAreaInset.top = rootView.getTop() + rootView.getPaddingTop();
+    safeAreaInset.bottom = rootView.getBottom() - rootView.getPaddingBottom();
+    safeAreaInset.right = rootView.getRight() - rootView.getPaddingRight();
+    calculateAdditionalInset(safeAreaInset);
+    getTemplateContext().getSurfaceInfoProvider().setVisibleArea(safeAreaInset);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java
new file mode 100644
index 0000000..679b90e
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplatePresenter.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.view;
+
+import static android.view.View.VISIBLE;
+import static java.lang.Math.max;
+
+import android.graphics.Insets;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.view.ViewTreeObserver.OnTouchModeChangeListener;
+import android.view.WindowInsets;
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleRegistry;
+import com.android.car.libraries.apphost.common.StatusBarManager.StatusBarState;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.android.car.libraries.apphost.logging.TelemetryEvent;
+import com.android.car.libraries.apphost.logging.TelemetryEvent.UiAction;
+import com.android.car.libraries.apphost.logging.TelemetryHandler;
+import java.util.List;
+
+/**
+ * Abstract base class for {@link TemplatePresenter}s which implements some of the common presenter
+ * functionality.
+ */
+public abstract class AbstractTemplatePresenter implements TemplatePresenter {
+  /**
+   * Test-only override for {@link #hasWindowFocus()}, since robolectric does not set the window
+   * focus properly.
+   */
+  @VisibleForTesting public Boolean mHasWindowFocusOverride;
+
+  private final TemplateContext mTemplateContext;
+  private final LifecycleRegistry mLifecycleRegistry;
+  private final StatusBarState mStatusBarState;
+
+  private TemplateWrapper mTemplateWrapper;
+
+  /** The last focused view before the presenter was refreshed. */
+  @Nullable private View mLastFocusedView;
+
+  /**
+   * Returns a callback called when the presenter view's touch mode changes.
+   *
+   * @see #restoreFocus() for details on how we work around a focus-related GMS core bug
+   */
+  private final OnTouchModeChangeListener mOnTouchModeChangeListener =
+      new OnTouchModeChangeListener() {
+        @SuppressWarnings("nullness") // suppress under initialization warning for this
+        @Override
+        public void onTouchModeChanged(boolean isInTouchMode) {
+          if (!isInTouchMode) {
+            restoreFocus();
+          }
+        }
+      };
+
+  /**
+   * Returns a callback called when the presenter view's focus changes.
+   *
+   * @see #restoreFocus() for details on how we work around a focus-related GMS core bug
+   */
+  private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
+      new OnGlobalFocusChangeListener() {
+        @SuppressWarnings("nullness") // suppress under initialization warning for this
+        @Override
+        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+          if (newFocus != null) {
+            setLastFocusedView(newFocus);
+          }
+        }
+      };
+
+  /**
+   * Constructs a new instance of a {@link AbstractTemplatePresenter} with the given {@link
+   * Template}.
+   */
+  // Suppress under-initialization checker warning for passing this to the LifecycleRegistry's
+  // ctor.
+  @SuppressWarnings({"nullness:assignment", "nullness:argument"})
+  public AbstractTemplatePresenter(
+      TemplateContext templateContext,
+      TemplateWrapper templateWrapper,
+      StatusBarState statusBarState) {
+    mTemplateContext = templateContext;
+    mTemplateWrapper = TemplateWrapper.copyOf(templateWrapper);
+    mStatusBarState = statusBarState;
+    mLifecycleRegistry = new LifecycleRegistry(this);
+  }
+
+  /** Sets the template this presenter will produce the views for. */
+  @Override
+  public void setTemplate(TemplateWrapper templateWrapper) {
+    mTemplateWrapper = TemplateWrapper.copyOf(templateWrapper);
+
+    onTemplateChanged();
+
+    if (!templateWrapper.isRefresh()) {
+      // Some presenters may get reused even if the template is not a refresh of the previous one.
+      // In those instances, we want the focus to be set to where the default focus should be
+      // instead of last focussed element. Specifically, we want to clear existing focus first,
+      // because if the previous focus was on a row item, and the list is reused and scrolled
+      // to the top, calling setDefaultFocus itself would not reset the focus back to the first
+      // row item.
+      getView().clearFocus();
+      setDefaultFocus();
+    } else {
+      View focusedView = getView().findFocus();
+      if (focusedView != null && focusedView.getVisibility() == VISIBLE) {
+        setLastFocusedView(focusedView);
+      } else {
+        setDefaultFocus();
+      }
+    }
+  }
+
+  /** Returns the template associated with this presenter. */
+  @Override
+  public Template getTemplate() {
+    return mTemplateWrapper.getTemplate();
+  }
+
+  /**
+   * Returns the {@link TemplateWrapper} instance that wraps the template associated with this
+   * presenter.
+   *
+   * @see #getTemplate()
+   */
+  @Override
+  public TemplateWrapper getTemplateWrapper() {
+    return mTemplateWrapper;
+  }
+
+  /** Returns the {@link TemplateContext} instance associated with this presenter. */
+  @Override
+  public TemplateContext getTemplateContext() {
+    return mTemplateContext;
+  }
+
+  @Override
+  @CallSuper
+  public void onCreate() {
+    L.d(LogTags.TEMPLATE, "Presenter onCreate: %s", this);
+    mLifecycleRegistry.setCurrentState(State.CREATED);
+  }
+
+  @Override
+  @CallSuper
+  public void onDestroy() {
+    L.d(LogTags.TEMPLATE, "Presenter onDestroy: %s", this);
+    mLifecycleRegistry.setCurrentState(State.DESTROYED);
+  }
+
+  @Override
+  @CallSuper
+  public void onStart() {
+    L.d(LogTags.TEMPLATE, "Presenter onStart: %s", this);
+    mLifecycleRegistry.setCurrentState(State.STARTED);
+  }
+
+  @Override
+  @CallSuper
+  public void onStop() {
+    L.d(LogTags.TEMPLATE, "Presenter onStop: %s", this);
+    mLifecycleRegistry.setCurrentState(State.CREATED);
+  }
+
+  @Override
+  @CallSuper
+  public void onResume() {
+    L.d(LogTags.TEMPLATE, "Presenter onResume: %s", this);
+    mLifecycleRegistry.setCurrentState(State.RESUMED);
+    mTemplateContext.getStatusBarManager().setStatusBarState(mStatusBarState, getView());
+
+    ViewTreeObserver viewTreeObserver = getView().getViewTreeObserver();
+    viewTreeObserver.addOnTouchModeChangeListener(mOnTouchModeChangeListener);
+    viewTreeObserver.addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+  }
+
+  @Override
+  @CallSuper
+  public void onPause() {
+    L.d(LogTags.TEMPLATE, "Presenter onPause: %s", this);
+    mLifecycleRegistry.setCurrentState(State.STARTED);
+
+    ViewTreeObserver viewTreeObserver = getView().getViewTreeObserver();
+    viewTreeObserver.removeOnTouchModeChangeListener(mOnTouchModeChangeListener);
+    viewTreeObserver.removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+
+    if (mTemplateContext.getColorContrastCheckState().checksContrast()) {
+      sendColorContrastTelemetryEvent(mTemplateContext, getTemplate().getClass().getSimpleName());
+    }
+  }
+
+  @Override
+  public void applyWindowInsets(WindowInsets windowInsets, int minimumTopPadding) {
+    int leftInset;
+    int topInset;
+    int rightInset;
+    int bottomInset;
+    if (VERSION.SDK_INT >= VERSION_CODES.R) {
+      Insets insets =
+          windowInsets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime());
+      leftInset = insets.left;
+      topInset = insets.top;
+      rightInset = insets.right;
+      bottomInset = insets.bottom;
+
+    } else {
+      leftInset = windowInsets.getSystemWindowInsetLeft();
+      topInset = windowInsets.getSystemWindowInsetTop();
+      rightInset = windowInsets.getSystemWindowInsetRight();
+      bottomInset = windowInsets.getSystemWindowInsetBottom();
+    }
+
+    View v = getView();
+    v.setPadding(leftInset, max(topInset, minimumTopPadding), rightInset, bottomInset);
+  }
+
+  @Override
+  public boolean setDefaultFocus() {
+    View defaultFocusedView = getDefaultFocusedView();
+    if (defaultFocusedView != null) {
+      defaultFocusedView.requestFocus();
+      setLastFocusedView(defaultFocusedView);
+    }
+    return true;
+  }
+
+  @Override
+  public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+    return false;
+  }
+
+  @Override
+  public boolean onPreDraw() {
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return "["
+        + Integer.toHexString(hashCode())
+        + ": "
+        + mTemplateWrapper.getTemplate().getClass().getSimpleName()
+        + "]";
+  }
+
+  /** Indicates that the template set in the presenter has changed. */
+  public abstract void onTemplateChanged();
+
+  @Override
+  public Lifecycle getLifecycle() {
+    return mLifecycleRegistry;
+  }
+
+  @Override
+  public boolean handlesTemplateChangeAnimation() {
+    return false;
+  }
+
+  @Override
+  public boolean isFullScreen() {
+    return true;
+  }
+
+  @Override
+  public boolean usesSurface() {
+    return false;
+  }
+
+  /**
+   * Restores the presenter's focus to the last focused view.
+   *
+   * <p>Note: A bug in GMS core causes {@link View#isInTouchMode()} to return {@code true} even in
+   * rotary or touchpad mode (b/128031459). When {@link View#layout(int, int, int, int)} is called,
+   * the focus is cleared if {@link View#isInTouchMode()} returns {@code true}. Because the correct
+   * touch mode value is eventually set, we can work around this issue by setting the {@link
+   * #mLastFocusedView} in when the focus changes and restoring the focus when the touch mode is
+   * {@code false} in a {@link OnTouchModeChangeListener}.
+   *
+   * <p>We call {@link #setLastFocusedView(View)} in these places:
+   *
+   * <ul>
+   *   <li>In {@link #setDefaultFocus()}: after the presenter is created.
+   *   <li>In {@link #setTemplate(TemplateWrapper)}: when the presenter is updated.
+   *   <li>In {@link #mOnGlobalFocusChangeListener}: when the user moves the focus in the presenter.
+   * </ul>
+   */
+  @VisibleForTesting
+  public void restoreFocus() {
+    View view = mLastFocusedView;
+    if (view != null) {
+      view.requestFocus();
+    }
+  }
+
+  /**
+   * Moves focus to one of the {@code toViews} if the focus is present in one of the {@code
+   * fromViews}.
+   *
+   * <p>The focus will move to the first view in {@code toViews} that can take focus.
+   *
+   * @return {@code true} if the focus has been moved, otherwise {@code false}
+   */
+  protected static boolean moveFocusIfPresent(List<View> fromViews, List<View> toViews) {
+    for (View fromView : fromViews) {
+      if (fromView.hasFocus()) {
+        for (View toView : toViews) {
+          if (toView.getVisibility() == VISIBLE && toView.requestFocus()) {
+            return true;
+          }
+        }
+        return false;
+      }
+    }
+    return false;
+  }
+
+  /** Returns whether the window containing the presenter's view has focus. */
+  protected boolean hasWindowFocus() {
+    if (mHasWindowFocusOverride != null) {
+      return mHasWindowFocusOverride;
+    }
+
+    return getView().hasWindowFocus();
+  }
+
+  /** Returns the view that should get focus by default. */
+  protected View getDefaultFocusedView() {
+    return getView();
+  }
+
+  /**
+   * Sets the presenter's last focused view.
+   *
+   * @see #restoreFocus() for details on how we work around a focus-related GMS core bug
+   */
+  private void setLastFocusedView(View focusedView) {
+    mLastFocusedView = focusedView;
+  }
+
+  private static void sendColorContrastTelemetryEvent(
+      TemplateContext templateContext, String templateClassName) {
+    TelemetryHandler telemetryHandler = templateContext.getTelemetryHandler();
+    telemetryHandler.logCarAppTelemetry(
+        TelemetryEvent.newBuilder(
+                templateContext.getColorContrastCheckState().getCheckPassed()
+                    ? UiAction.COLOR_CONTRAST_CHECK_PASSED
+                    : UiAction.COLOR_CONTRAST_CHECK_FAILED,
+                templateContext.getCarAppPackageInfo().getComponentName())
+            .setTemplateClassName(templateClassName));
+
+    // Reset color contrast check state
+    templateContext.getColorContrastCheckState().setCheckPassed(true);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java
new file mode 100644
index 0000000..53aa804
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/AbstractTemplateView.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.libraries.apphost.view;
+
+import static com.android.car.libraries.apphost.common.EventManager.EventType.TEMPLATE_TOUCHED_OR_FOCUSED;
+import static com.android.car.libraries.apphost.common.EventManager.EventType.WINDOW_FOCUS_CHANGED;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.ViewTreeObserver.OnWindowFocusChangeListener;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+import androidx.annotation.MainThread;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.model.Template;
+import androidx.car.app.model.TemplateWrapper;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import com.android.car.libraries.apphost.common.TemplateContext;
+import com.android.car.libraries.apphost.common.ThreadUtils;
+import com.android.car.libraries.apphost.logging.L;
+import com.android.car.libraries.apphost.logging.LogTags;
+import com.google.common.base.Preconditions;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A view that displays {@link Template}s.
+ *
+ * <p>The current template can be set with {@link #setTemplate} method.
+ */
+public abstract class AbstractTemplateView extends FrameLayout {
+  /**
+   * The {@link TemplatePresenter} for the template currently set in the view or {@code null} if
+   * none is set.
+   */
+  @Nullable private TemplatePresenter mCurrentPresenter;
+
+  /** The {@link Lifecycle} object of the parent of this view (e.g. the car activity hosting it). */
+  @MonotonicNonNull private Lifecycle mParentLifecycle;
+
+  /**
+   * An observer for the {@link #mParentLifecycle}, which is registered and unregistered when the
+   * view is attached and detached.
+   */
+  @Nullable private LifecycleObserver mLifecycleObserver;
+
+  /**
+   * Context for various {@link TemplatePresenter}s to retrieve important bits of information for
+   * presenting content.
+   */
+  @MonotonicNonNull private TemplateContext mTemplateContext;
+
+  /** {@link WindowInsets} to apply to templates. */
+  @MonotonicNonNull private WindowInsets mWindowInsets;
+
+  /**
+   * The window focus value in the last callback from the {@link OnWindowFocusChangeListener}.
+   *
+   * <p>We need to store this value because the listener is called even if the window focus state
+   * does not change, when the view focus moves.
+   */
+  private boolean mLastWindowFocusState;
+
+  /** A callback called when the template view's window focus changes. */
+  private final OnWindowFocusChangeListener mOnWindowFocusChangeListener =
+      new OnWindowFocusChangeListener() {
+        @SuppressWarnings("nullness") // suppress under initialization warning for this
+        @Override
+        public void onWindowFocusChanged(boolean hasFocus) {
+          if (hasFocus != mLastWindowFocusState) {
+            // Dispatch the window focus event only when the window focus state changes.
+            dispatchWindowFocusEvent();
+            mLastWindowFocusState = hasFocus;
+          }
+        }
+      };
+
+  private final OnPreDrawListener mOnPreDrawListener =
+      new OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+          TemplatePresenter presenter = mCurrentPresenter;
+          if (presenter != null) {
+            return presenter.onPreDraw();
+          }
+          return true;
+        }
+      };
+
+  protected AbstractTemplateView(Context context) {
+    this(context, null);
+  }
+
+  protected AbstractTemplateView(Context context, @Nullable AttributeSet attrs) {
+    this(context, attrs, 0);
+  }
+
+  protected AbstractTemplateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+    super(context, attrs, defStyleAttr);
+  }
+
+  /**
+   * Returns the {@link SurfaceViewContainer} which holds the surface that 3p apps can use to render
+   * custom content.
+   */
+  protected abstract SurfaceViewContainer getSurfaceViewContainer();
+
+  /** Returns the {@link FrameLayout} container which holds the currently set template. */
+  protected abstract ViewGroup getTemplateContainer();
+
+  /**
+   * Returns the minimum top padding to use when laying out the UI.
+   *
+   * <p>This is used to ensure there is some spacing from top of the screen to the UI when there is
+   * no status bar (i.e. widescreen).
+   */
+  protected abstract int getMinimumTopPadding();
+
+  /**
+   * Returns a {@link TemplateTransitionManager} responsible for handling transitions between
+   * presenters
+   */
+  protected abstract TemplateTransitionManager getTransitionManager();
+
+  /** Returns the current {@link TemplateContext} or {@code null} if one has not been set. */
+  @Nullable
+  protected TemplateContext getTemplateContext() {
+    return mTemplateContext;
+  }
+
+  /**
+   * Returns a {@link SurfaceProvider} which can be used to retrieve the {@link
+   * android.view.Surface} that 3p apps can use to draw custom content.
+   */
+  public SurfaceProvider getSurfaceProvider() {
+    return getSurfaceViewContainer();
+  }
+
+  /**
+   * Sets the parent {@link Lifecycle} for this view.
+   *
+   * <p>This is normally the activity or fragment the view is attached to.
+   */
+  public void setParentLifecycle(Lifecycle parentLifecycle) {
+    mParentLifecycle = parentLifecycle;
+  }
+
+  /** Returns the parent {@link Lifecycle}. */
+  protected @Nullable Lifecycle getParentLifecycle() {
+    return mParentLifecycle;
+  }
+
+  /** Sets the {@link TemplateContext} for this view. */
+  public void setTemplateContext(TemplateContext templateContext) {
+    mTemplateContext = TemplateContext.from(templateContext, getContext());
+  }
+
+  /** Stores the window insets to apply to templates. */
+  public void setWindowInsets(WindowInsets windowInsets) {
+    mWindowInsets = windowInsets;
+    if (mCurrentPresenter != null) {
+      mCurrentPresenter.applyWindowInsets(windowInsets, getMinimumTopPadding());
+    }
+  }
+
+  @Override
+  public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
+    dispatchTouchFocusEvent();
+    if (mCurrentPresenter != null && mCurrentPresenter.onKeyUp(keyCode, keyEvent)) {
+      return true;
+    }
+    return super.onKeyUp(keyCode, keyEvent);
+  }
+
+  @Override
+  public boolean onInterceptTouchEvent(MotionEvent ev) {
+    dispatchTouchFocusEvent();
+    return super.onInterceptTouchEvent(ev);
+  }
+
+  @Override
+  public boolean onGenericMotionEvent(MotionEvent motionEvent) {
+    dispatchTouchFocusEvent();
+    return super.onGenericMotionEvent(motionEvent);
+  }
+
+  @Override
+  @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+  public void onAttachedToWindow() {
+    super.onAttachedToWindow();
+    if (mParentLifecycle != null) {
+      initLifecycleObserver(mParentLifecycle);
+    }
+
+    ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+    viewTreeObserver.addOnWindowFocusChangeListener(mOnWindowFocusChangeListener);
+    viewTreeObserver.addOnPreDrawListener(mOnPreDrawListener);
+  }
+
+  /** Returns the presenter currently attached to this view. */
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  @Nullable
+  public TemplatePresenter getCurrentPresenter() {
+    return mCurrentPresenter;
+  }
+
+  /** Sets the {@link Template} to display in the view, or {@code null} to display nothing. */
+  @MainThread
+  public void setTemplate(TemplateWrapper templateWrapper) {
+    ThreadUtils.ensureMainThread();
+
+    // First convert the template to another template type if needed.
+    templateWrapper =
+        TemplateConverterRegistry.get().maybeConvertTemplate(getContext(), templateWrapper);
+
+    TemplatePresenter previousPresenter = mCurrentPresenter;
+    if (mCurrentPresenter != null) {
+      TemplatePresenter presenter = mCurrentPresenter;
+
+      Template template = templateWrapper.getTemplate();
+
+      // Allow the existing presenter to update the views if:
+      //   1) Both the previous and the new template are  of the same class.
+      //   2) The new template is a refresh OR the presenter handles the template change
+      // animation.
+      boolean updatePresenter = presenter.getTemplate().getClass().equals(template.getClass());
+      updatePresenter &= templateWrapper.isRefresh() || presenter.handlesTemplateChangeAnimation();
+      if (updatePresenter) {
+        updatePresenter(presenter, templateWrapper);
+        return;
+      }
+
+      // The current presenter is not of the same type as the given template, so remove it. We
+      // will create a new presenter below and re-add it if needed.
+      // TODO(b/151953922): Test the ordering of pause, remove view, destroy.
+      pausePresenter(presenter);
+      stopPresenter(presenter);
+      destroyPresenter(presenter);
+      mCurrentPresenter = null;
+    }
+
+    TemplatePresenter presenter = createPresenter(templateWrapper);
+    mCurrentPresenter = presenter;
+    transition(presenter, previousPresenter);
+
+    if (presenter != null) {
+      presenter.setDefaultFocus();
+    }
+  }
+
+  private void transition(@Nullable TemplatePresenter to, @Nullable TemplatePresenter from) {
+    if (to != null) {
+      getTransitionManager()
+          .transition(getTemplateContainer(), getSurfaceViewContainer(), to, from);
+    } else {
+      getSurfaceViewContainer().setVisibility(GONE);
+      View previousView = from == null ? null : from.getView();
+      if (previousView != null) {
+        getTemplateContainer().removeView(previousView);
+      }
+    }
+  }
+
+  /**
+   * Returns the {@link WindowInsets} to apply to the templates presented by the view or {@code
+   * null} if not set.
+   *
+   * @see #setWindowInsets(WindowInsets)
+   */
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  @Nullable
+  public WindowInsets getWindowInsets() {
+    return mWindowInsets;
+  }
+
+  @Override
+  @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+  public void onDetachedFromWindow() {
+    ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+    viewTreeObserver.removeOnWindowFocusChangeListener(mOnWindowFocusChangeListener);
+    viewTreeObserver.removeOnPreDrawListener(mOnPreDrawListener);
+
+    // Stop the presenter, since its view is no longer visible.
+    TemplatePresenter presenter = mCurrentPresenter;
+    if (presenter != null) {
+      stopPresenter(presenter);
+    }
+
+    if (mLifecycleObserver != null && mParentLifecycle != null) {
+      mParentLifecycle.removeObserver(mLifecycleObserver);
+      mLifecycleObserver = null;
+    }
+
+    super.onDetachedFromWindow();
+  }
+
+  /**
+   * Let any listeners know that an(y) UI element within the template view has been interacted on,
+   * either via touch or focus events.
+   */
+  private void dispatchTouchFocusEvent() {
+    TemplateContext context = mTemplateContext;
+    if (context != null) {
+      context.getEventManager().dispatchEvent(TEMPLATE_TOUCHED_OR_FOCUSED);
+    }
+  }
+
+  /**
+   * Let any listeners know that the window that contains the template view has changed its focus
+   * state.
+   */
+  private void dispatchWindowFocusEvent() {
+    TemplateContext context = mTemplateContext;
+    if (context != null) {
+      context.getEventManager().dispatchEvent(WINDOW_FOCUS_CHANGED);
+    }
+  }
+
+  /** Updates the given presenter with the data from the given template. */
+  private static void updatePresenter(
+      TemplatePresenter presenter, TemplateWrapper templateWrapper) {
+    Preconditions.checkState(
+        presenter.getTemplate().getClass().equals(templateWrapper.getTemplate().getClass()));
+
+    L.d(LogTags.TEMPLATE, "Updating presenter: %s", presenter);
+    presenter.setTemplate(templateWrapper);
+  }
+
+  /** Pauses the given presenter. */
+  private static void pausePresenter(TemplatePresenter presenter) {
+    L.d(LogTags.TEMPLATE, "Pausing presenter: %s", presenter);
+
+    State currentState = presenter.getLifecycle().getCurrentState();
+    if (currentState.isAtLeast(State.RESUMED)) {
+      presenter.onPause();
+    }
+  }
+
+  /** Stops the given presenter. */
+  private static void stopPresenter(TemplatePresenter presenter) {
+    L.d(LogTags.TEMPLATE, "Stopping presenter: %s", presenter);
+
+    State currentState = presenter.getLifecycle().getCurrentState();
+    if (currentState.isAtLeast(State.STARTED)) {
+      presenter.onStop();
+    }
+  }
+
+  /** Destroys the given presenter. */
+  private static void destroyPresenter(TemplatePresenter presenter) {
+    L.d(LogTags.TEMPLATE, "Destroying presenter: %s", presenter);
+
+    presenter.onDestroy();
+  }
+
+  /**
+   * Creates and starts a new presenter for the given template or {@code null} if a presenter could
+   * not be found for it.
+   */
+  @Nullable
+  private TemplatePresenter createPresenter(TemplateWrapper templateWrapper) {
+    if (mTemplateContext == null) {
+      throw new IllegalStateException(
+          "templateContext is null when attempting to create a presenter");
+    }
+
+    TemplatePresenter presenter =
+        TemplatePresenterRegistry.get().createPresenter(mTemplateContext, templateWrapper);
+    if (presenter == null) {
+      L.w(
+          LogTags.TEMPLATE,
+          "No presenter available for template type: %s",
+          templateWrapper.getTemplate().getClass().getSimpleName());
+      return null;
+    }
+
+    L.d(LogTags.TEMPLATE, "Creating new presenter: %s", presenter);
+    presenter.onCreate();
+
+    if (mParentLifecycle != null) {
+      // Only start and resume it if our parent parent lifecycle is in those states. If not,
+      // we will
+      // switch to the state when/if the parent lifecycle reaches it later on.
+      State parentState = mParentLifecycle.getCurrentState();
+      if (parentState.isAtLeast(State.STARTED)) {
+        presenter.onStart();
+      }
+      if (parentState.isAtLeast(State.RESUMED)) {
+        presenter.onResume();
+      }
+    }
+
+    if (mWindowInsets != null) {
+      presenter.applyWindowInsets(mWindowInsets, getMinimumTopPadding());
+    }
+    return presenter;
+  }
+
+  /**
+   * Instantiates a parent lifecycle observer that forwards the relevant events to the current
+   * presenter.
+   */
+  private void initLifecycleObserver(Lifecycle parentLifecycle) {
+    mLifecycleObserver =
+        new DefaultLifecycleObserver() {
+          @Override
+          public void onStart(LifecycleOwner lifecycleOwner) {
+            if (mCurrentPresenter != null) {
+              mCurrentPresenter.onStart();
+            }
+          }
+
+          @Override
+          public void onStop(LifecycleOwner lifecycleOwner) {
+            if (mCurrentPresenter != null) {
+              mCurrentPresenter.onStop();
+            }
+          }
+
+          @Override
+          public void onResume(LifecycleOwner lifecycleOwner) {
+            if (mCurrentPresenter != null) {
+              mCurrentPresenter.onResume();
+            }
+          }
+
+          @Override
+          public void onPause(LifecycleOwner lifecycleOwner) {
+            if (mCurrentPresenter != null) {
+              mCurrentPresenter.onPause();
+            }
+          }
+
+          @Override
+          public void onDestroy(LifecycleOwner lifecycleOwner) {
+            if (mCurrentPresenter != null) {
+              mCurrentPresenter.onDestroy();
+            }
+          }
+        };
+    parentLifecycle.addObserver(mLifecycleObserver);
+  }
+}
diff --git a/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java
new file mode 100644
index 0000000..ab61aa9
--- /dev/null
+++ b/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/view/PanZoomManager.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specifi