summaryrefslogtreecommitdiff
path: root/Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common
diff options
context:
space:
mode:
Diffstat (limited to 'Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common')
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ANRHandler.java51
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ApiIncompatibilityType.java22
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppBindingStateProvider.java32
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppDispatcher.java230
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppHostService.java20
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppIconLoader.java35
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/AppServiceCall.java30
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/BackPressedHandler.java27
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppColors.java48
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppError.java181
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppManager.java32
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarAppPackageInfo.java42
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarColorUtils.java379
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CarHostConfig.java203
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorContrastCheckState.java34
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ColorUtils.java165
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/CommonUtils.java54
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/DebugOverlayHandler.java71
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorHandler.java25
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ErrorMessageTemplateBuilder.java157
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/EventManager.java139
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/HostResourceIds.java215
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IncompatibleApiException.java32
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/IntentUtils.java55
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/InvalidatedCarHostException.java25
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/LocationMediator.java75
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapGestureManager.java48
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapOnGestureListener.java293
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/MapViewContainer.java48
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/NamedAppServiceCall.java56
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OnDoneCallbackStub.java125
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/OneWayIPC.java34
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/RoutingInfoState.java27
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ScaleGestureDetector.java555
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StatusBarManager.java46
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/StringUtils.java72
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceCallbackHandler.java44
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SurfaceInfoProvider.java53
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/SystemClockWrapper.java35
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/TemplateContext.java332
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ThreadUtils.java70
-rw-r--r--Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common/ToastController.java31
42 files changed, 4248 insertions, 0 deletions
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);
+}