/* * 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 android.car.cluster; import static android.car.feature.Flags.FLAG_CLUSTER_HEALTH_MONITORING; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.app.Activity; import android.car.Car; import android.car.CarManagerBase; import android.car.builtin.util.Slogf; import android.car.builtin.view.SurfaceControlHelper; import android.content.Intent; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.view.SurfaceControl; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnPreDrawListener; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.ICarBase; import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; /** * Provides the api to manage {@code ClusterHome}. * * @hide */ @FlaggedApi(FLAG_CLUSTER_HEALTH_MONITORING) @SystemApi public final class ClusterHomeManager extends CarManagerBase { private static final String TAG = ClusterHomeManager.class.getSimpleName(); /** * When the client reports ClusterHome state and if there is no UI in the sub area, it can * reports UI_TYPE_CLUSTER_NONE instead. * * @hide */ public static final int UI_TYPE_CLUSTER_NONE = -1; /** @hide */ public static final int UI_TYPE_CLUSTER_HOME = 0; /** @hide */ @IntDef(flag = true, prefix = { "CONFIG_" }, value = { CONFIG_DISPLAY_ON_OFF, CONFIG_DISPLAY_BOUNDS, CONFIG_DISPLAY_INSETS, CONFIG_UI_TYPE, }) @Retention(RetentionPolicy.SOURCE) public @interface Config {} /** Bit fields indicates which fields of {@link ClusterState} are changed */ /** @hide */ public static final int CONFIG_DISPLAY_ON_OFF = 0x01; /** @hide */ public static final int CONFIG_DISPLAY_BOUNDS = 0x02; /** @hide */ public static final int CONFIG_DISPLAY_INSETS = 0x04; /** @hide */ public static final int CONFIG_UI_TYPE = 0x08; /** @hide */ public static final int CONFIG_DISPLAY_ID = 0x10; /** * Callback for ClusterHome to get notifications when cluster state changes. * * @hide */ public interface ClusterStateListener { /** * Called when ClusterOS changes the cluster display state, the geometry of cluster display, * or the uiType. * @param state newly updated {@link ClusterState} * @param changes the flag indicates which fields are updated */ void onClusterStateChanged(ClusterState state, @Config int changes); } /** * Callback for ClusterHome to get notifications when cluster navigation state changes. */ @FlaggedApi(FLAG_CLUSTER_HEALTH_MONITORING) public interface ClusterNavigationStateListener { /** * Called when the app who owns the navigation focus casts the new navigation state. * * @param navigationState Byte array that is serialized from a {@link * android.car.cluster.navigation.NavigationState.NavigationStateProto} proto value. */ void onNavigationStateChanged(@NonNull byte[] navigationState); } private static class ClusterStateListenerRecord { final Executor mExecutor; final ClusterStateListener mListener; ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) { mExecutor = executor; mListener = listener; } @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ClusterStateListenerRecord)) { return false; } return mListener == ((ClusterStateListenerRecord) obj).mListener; } @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) @Override public int hashCode() { return mListener.hashCode(); } } private static class ClusterNavigationStateListenerRecord { final Executor mExecutor; final ClusterNavigationStateListener mListener; ClusterNavigationStateListenerRecord(Executor executor, ClusterNavigationStateListener listener) { mExecutor = executor; mListener = listener; } @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ClusterNavigationStateListenerRecord)) { return false; } return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener; } @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) @Override public int hashCode() { return mListener.hashCode(); } } private final IClusterHomeService mService; private final IClusterStateListenerImpl mClusterStateListenerBinderCallback; private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback; private final CopyOnWriteArrayList mStateListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList mNavigationStateListeners = new CopyOnWriteArrayList<>(); private boolean mVisibilityMonitoringStarted = false; /** @hide */ @VisibleForTesting public ClusterHomeManager(ICarBase car, IBinder service) { super(car); mService = IClusterHomeService.Stub.asInterface(service); mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this); mClusterNavigationStateListenerBinderCallback = new IClusterNavigationStateListenerImpl(this); } /** * Registers the callback for ClusterHome. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void registerClusterStateListener( @NonNull Executor executor, @NonNull ClusterStateListener callback) { Objects.requireNonNull(executor, "executor cannot be null"); Objects.requireNonNull(callback, "callback cannot be null"); ClusterStateListenerRecord clusterStateListenerRecord = new ClusterStateListenerRecord(executor, callback); if (!mStateListeners.addIfAbsent(clusterStateListenerRecord)) { return; } if (mStateListeners.size() == 1) { try { mService.registerClusterStateListener(mClusterStateListenerBinderCallback); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } } /** * Registers a listener for navigation state changes. * *

Note that multiple listeners can be registered. All registered listeners are invoked * when the navigation app that has the focus sends a state change. *

A listener is invoked only for changes that occur after the registration. It is not * called for the previous or current states at the time of the registration. */ @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE) public void registerClusterNavigationStateListener( @NonNull Executor executor, @NonNull ClusterNavigationStateListener callback) { Objects.requireNonNull(executor, "executor cannot be null"); Objects.requireNonNull(callback, "callback cannot be null"); ClusterNavigationStateListenerRecord clusterStateListenerRecord = new ClusterNavigationStateListenerRecord(executor, callback); if (!mNavigationStateListeners.addIfAbsent(clusterStateListenerRecord)) { return; } if (mNavigationStateListeners.size() == 1) { try { mService.registerClusterNavigationStateListener( mClusterNavigationStateListenerBinderCallback); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } } /** * Unregisters the callback. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void unregisterClusterStateListener(@NonNull ClusterStateListener callback) { Objects.requireNonNull(callback, "callback cannot be null"); if (!mStateListeners .remove(new ClusterStateListenerRecord(/* executor= */ null, callback))) { return; } if (mStateListeners.isEmpty()) { try { mService.unregisterClusterStateListener(mClusterStateListenerBinderCallback); } catch (RemoteException ignored) { // ignore for unregistering } } } /** * Unregisters a listener for navigation state changes. */ @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE) public void unregisterClusterNavigationStateListener( @NonNull ClusterNavigationStateListener callback) { Objects.requireNonNull(callback, "callback cannot be null"); if (!mNavigationStateListeners.remove(new ClusterNavigationStateListenerRecord( /* executor= */ null, callback))) { return; } if (mNavigationStateListeners.isEmpty()) { try { mService.unregisterClusterNavigationStateListener( mClusterNavigationStateListenerBinderCallback); } catch (RemoteException ignored) { // ignore for unregistering } } } private static class IClusterStateListenerImpl extends IClusterStateListener.Stub { private final WeakReference mManager; private IClusterStateListenerImpl(ClusterHomeManager manager) { mManager = new WeakReference<>(manager); } @Override public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) { ClusterHomeManager manager = mManager.get(); if (manager != null) { for (ClusterStateListenerRecord cb : manager.mStateListeners) { cb.mExecutor.execute( () -> cb.mListener.onClusterStateChanged(state, changes)); } } } } private static class IClusterNavigationStateListenerImpl extends IClusterNavigationStateListener.Stub { private final WeakReference mManager; private IClusterNavigationStateListenerImpl(ClusterHomeManager manager) { mManager = new WeakReference<>(manager); } @Override public void onNavigationStateChanged(@NonNull byte[] navigationState) { ClusterHomeManager manager = mManager.get(); if (manager != null) { for (ClusterNavigationStateListenerRecord lr : manager.mNavigationStateListeners) { lr.mExecutor.execute( () -> lr.mListener.onNavigationStateChanged(navigationState)); } } } } /** * Reports the current ClusterUI state. * @param uiTypeMain uiType that ClusterHome tries to show in main area * @param uiTypeSub uiType that ClusterHome tries to show in sub area * @param uiAvailability the byte array to represent the availability of ClusterUI. * 0 indicates non-available and 1 indicates available. * Index 0 is reserved for ClusterHome, The other indexes are followed by OEM's definition. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability) { try { mService.reportState(uiTypeMain, uiTypeSub, uiAvailability); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Requests to turn the cluster display on to show some ClusterUI. * @param uiType uiType that ClusterHome tries to show in main area * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void requestDisplay(int uiType) { try { mService.requestDisplay(uiType); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Returns the current {@code ClusterState}. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) @Nullable public ClusterState getClusterState() { ClusterState state = null; try { state = mService.getClusterState(); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } return state; } /** * Start an activity as specified user. The activity is considered as in fixed mode for * the cluster display and will be re-launched if the activity crashes, the package * is updated or goes to background for whatever reason. * Only one activity can exist in fixed mode for the display and calling this multiple * times with different {@code Intent} will lead into making all previous activities into * non-fixed normal state (= will not be re-launched.) * @param intent the Intent to start * @param options additional options for how the Activity should be started * @param userId the user the new activity should run as * @return true if it launches the given Intent as FixedActivity successfully * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public boolean startFixedActivityModeAsUser( Intent intent, @Nullable Bundle options, int userId) { try { return mService.startFixedActivityModeAsUser(intent, options, userId); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } return false; } /** * The activity launched on the cluster display is no longer in fixed mode. Re-launching or * finishing should not trigger re-launching any more. Note that Activity for non-current user * will be auto-stopped and there is no need to call this for user switching. Note that this * does not stop the activity but it will not be re-launched any more. * * @hide */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void stopFixedActivityMode() { try { mService.stopFixedActivityMode(); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Sends a heartbeat to ClusterOS. * @param epochTimeNs the current time * @param appMetadata the application specific metadata which will be delivered with * the heartbeat. */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void sendHeartbeat(long epochTimeNs, @Nullable byte[] appMetadata) { try { mService.sendHeartbeat(epochTimeNs, appMetadata); } catch (RemoteException e) { handleRemoteExceptionFromCarService(e); } } /** * Starts the visibility monitoring of given {@link Activity}. * * Note: This is supposed to be called in {@link Activity#onStart()} generally. * * @param activity the {@link Activity} to track the visibility of its Window */ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) public void startVisibilityMonitoring(@NonNull Activity activity) { // We'd like to check the permission locally too, since the actual execution happens later. if (getContext().checkCallingOrSelfPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) != PERMISSION_GRANTED) { throw new SecurityException( "requires permission " + Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); } if (mVisibilityMonitoringStarted) { Slogf.w(TAG, "startVisibilityMonitoring is already started"); return; } if (SurfaceControlHelper.getSurfaceControl(activity) != null) { throw new IllegalStateException( "startVisibilityMonitoring is expected to be called before onAttachedToWindow"); } mVisibilityMonitoringStarted = true; ViewTreeObserver observer = getViewTreeObserver(activity); // Can't use onWindowAttached, because SurfaceControl is available at the time, but invalid. // TODO: b/286406553 - Move the callback below to onWindowAttached. observer.addOnPreDrawListener( new OnPreDrawListener() { @Override public boolean onPreDraw() { // The existing 'observer' would be invalid, so gets it again. ViewTreeObserver observer = getViewTreeObserver(activity); observer.removeOnPreDrawListener(this); startVisibilityMonitoringInternal(activity); return true; } }); observer.addOnWindowAttachListener( new ViewTreeObserver.OnWindowAttachListener() { @Override public void onWindowAttached() { // Using onPreDraw instead, check b/286406553. } @Override public void onWindowDetached() { ViewTreeObserver observer = getViewTreeObserver(activity); observer.removeOnWindowAttachListener(this); stopVisibilityMonitoringInternal(); } } ); } private static ViewTreeObserver getViewTreeObserver(@NonNull Activity activity) { return activity.getWindow().getDecorView().getViewTreeObserver(); } private void startVisibilityMonitoringInternal(Activity activity) { SurfaceControl surfaceControl = SurfaceControlHelper.getSurfaceControl(activity); try { mService.startVisibilityMonitoring(surfaceControl); } catch (RemoteException e) { Slogf.e(TAG, "Failed to startVisibilityMonitoring", e); } } private void stopVisibilityMonitoringInternal() { try { mService.stopVisibilityMonitoring(); mVisibilityMonitoringStarted = false; } catch (RemoteException e) { Slogf.e(TAG, "Failed to stopVisibilityMonitoring", e); } } /** @hide */ @Override protected void onCarDisconnected() { mStateListeners.clear(); mNavigationStateListeners.clear(); } }