| /* |
| * 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 android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.car.Car; |
| import android.car.CarManagerBase; |
| import android.content.Intent; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| |
| 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; |
| |
| /** @hide */ |
| public 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. |
| */ |
| public static final int UI_TYPE_CLUSTER_NONE = -1; |
| 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 */ |
| public static final int CONFIG_DISPLAY_ON_OFF = 0x01; |
| public static final int CONFIG_DISPLAY_BOUNDS = 0x02; |
| public static final int CONFIG_DISPLAY_INSETS = 0x04; |
| public static final int CONFIG_UI_TYPE = 0x08; |
| public static final int CONFIG_DISPLAY_ID = 0x10; |
| |
| /** |
| * Callback for ClusterHome to get notifications when cluster state changes. |
| */ |
| 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. |
| */ |
| public interface ClusterNavigationStateListener { |
| /** Called when the App who owns the navigation focus casts the new navigation state. */ |
| void onNavigationState(byte[] navigationState); |
| } |
| |
| private static class ClusterStateListenerRecord { |
| final Executor mExecutor; |
| final ClusterStateListener mListener; |
| ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) { |
| mExecutor = executor; |
| mListener = listener; |
| } |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof ClusterStateListenerRecord)) { |
| return false; |
| } |
| return mListener == ((ClusterStateListenerRecord) obj).mListener; |
| } |
| @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; |
| } |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof ClusterNavigationStateListenerRecord)) { |
| return false; |
| } |
| return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener; |
| } |
| @Override |
| public int hashCode() { |
| return mListener.hashCode(); |
| } |
| } |
| |
| private final IClusterHomeService mService; |
| private final IClusterStateListenerImpl mClusterStateListenerBinderCallback; |
| private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback; |
| private final CopyOnWriteArrayList<ClusterStateListenerRecord> mStateListeners = |
| new CopyOnWriteArrayList<>(); |
| private final CopyOnWriteArrayList<ClusterNavigationStateListenerRecord> |
| mNavigationStateListeners = new CopyOnWriteArrayList<>(); |
| |
| /** @hide */ |
| @VisibleForTesting |
| public ClusterHomeManager(Car car, IBinder service) { |
| super(car); |
| mService = IClusterHomeService.Stub.asInterface(service); |
| mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this); |
| mClusterNavigationStateListenerBinderCallback = |
| new IClusterNavigationStateListenerImpl(this); |
| } |
| |
| /** |
| * Registers the callback for ClusterHome. |
| */ |
| @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 the callback for ClusterHome. |
| */ |
| @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. |
| */ |
| @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 the callback. |
| */ |
| @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<ClusterHomeManager> 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<ClusterHomeManager> 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.onNavigationState(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. |
| */ |
| @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 |
| */ |
| @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}. |
| */ |
| @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 |
| */ |
| @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. |
| */ |
| @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) |
| public void stopFixedActivityMode() { |
| try { |
| mService.stopFixedActivityMode(); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| } |
| |
| @Override |
| protected void onCarDisconnected() { |
| mStateListeners.clear(); |
| mNavigationStateListeners.clear(); |
| } |
| } |