diff options
Diffstat (limited to 'Host/app/apphost/src/main/java/com/android/car/libraries/apphost/common')
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); +} |