| /* |
| * 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.cluster; |
| |
| import static android.car.builtin.app.ActivityManagerHelper.createActivityOptions; |
| import static android.content.Intent.ACTION_MAIN; |
| |
| import static com.android.car.hal.ClusterHalService.DISPLAY_OFF; |
| import static com.android.car.hal.ClusterHalService.DISPLAY_ON; |
| import static com.android.car.hal.ClusterHalService.DONT_CARE; |
| import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; |
| import static com.android.car.internal.common.CommonConstants.EMPTY_BYTE_ARRAY; |
| |
| import android.app.ActivityOptions; |
| import android.car.Car; |
| import android.car.CarOccupantZoneManager; |
| import android.car.ICarOccupantZoneCallback; |
| import android.car.builtin.app.TaskInfoHelper; |
| import android.car.builtin.os.UserManagerHelper; |
| import android.car.builtin.util.Slogf; |
| import android.car.cluster.ClusterHomeManager; |
| import android.car.cluster.ClusterState; |
| import android.car.cluster.IClusterHomeService; |
| import android.car.cluster.IClusterNavigationStateListener; |
| import android.car.cluster.IClusterStateListener; |
| import android.car.cluster.navigation.NavigationState.NavigationStateProto; |
| import android.car.navigation.CarNavigationInstrumentCluster; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.graphics.Insets; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.hardware.display.DisplayManager; |
| import android.os.Bundle; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.text.TextUtils; |
| import android.util.proto.ProtoOutputStream; |
| import android.view.Display; |
| |
| import com.android.car.CarLog; |
| import com.android.car.CarOccupantZoneService; |
| import com.android.car.CarServiceBase; |
| import com.android.car.R; |
| import com.android.car.am.CarActivityService; |
| import com.android.car.am.FixedActivityService; |
| import com.android.car.hal.ClusterHalService; |
| import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; |
| import com.android.car.internal.util.IndentingPrintWriter; |
| |
| /** |
| * Service responsible for interactions between ClusterOS and ClusterHome. |
| */ |
| public class ClusterHomeService extends IClusterHomeService.Stub |
| implements CarServiceBase, ClusterNavigationService.ClusterNavigationServiceCallback, |
| ClusterHalService.ClusterHalEventCallback { |
| private static final String TAG = CarLog.TAG_CLUSTER; |
| private static final int DEFAULT_MIN_UPDATE_INTERVAL_MILLIS = 1000; |
| private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2"; |
| |
| private final Context mContext; |
| private final ClusterHalService mClusterHalService; |
| private final ClusterNavigationService mClusterNavigationService; |
| private final CarOccupantZoneService mOccupantZoneService; |
| private final FixedActivityService mFixedActivityService; |
| private final CarActivityService mCarActivityService; |
| private final ComponentName mClusterHomeActivity; |
| |
| private boolean mServiceEnabled; |
| |
| private int mClusterDisplayId = Display.INVALID_DISPLAY; |
| |
| private int mOnOff = DISPLAY_OFF; |
| private Rect mBounds = new Rect(); |
| private Insets mInsets = Insets.NONE; |
| private int mUiType = ClusterHomeManager.UI_TYPE_CLUSTER_HOME; |
| private Intent mLastIntent; |
| private int mLastIntentUserId = UserManagerHelper.USER_SYSTEM; |
| private volatile boolean mClusterActivityVisible; |
| |
| private final RemoteCallbackList<IClusterStateListener> mClientListeners = |
| new RemoteCallbackList<>(); |
| |
| private final RemoteCallbackList<IClusterNavigationStateListener> mClientNavigationListeners = |
| new RemoteCallbackList<>(); |
| |
| public ClusterHomeService(Context context, ClusterHalService clusterHalService, |
| ClusterNavigationService navigationService, |
| CarOccupantZoneService occupantZoneService, |
| FixedActivityService fixedActivityService, |
| CarActivityService carActivityService) { |
| mContext = context; |
| mClusterHalService = clusterHalService; |
| mClusterNavigationService = navigationService; |
| mOccupantZoneService = occupantZoneService; |
| mFixedActivityService = fixedActivityService; |
| mCarActivityService = carActivityService; |
| mClusterHomeActivity = ComponentName.unflattenFromString( |
| mContext.getString(R.string.config_clusterHomeActivity)); |
| mLastIntent = new Intent(ACTION_MAIN).setComponent(mClusterHomeActivity); |
| } |
| |
| @Override |
| public void init() { |
| Slogf.d(TAG, "initClusterHomeService"); |
| if (TextUtils.isEmpty(mClusterHomeActivity.getPackageName()) |
| || TextUtils.isEmpty(mClusterHomeActivity.getClassName())) { |
| Slogf.i(TAG, "Improper ClusterHomeActivity: %s", mClusterHomeActivity); |
| return; |
| } |
| if (!mClusterHalService.isServiceEnabled()) { |
| Slogf.e(TAG, "ClusterHomeService is disabled. To enable, it must be either in LIGHT " |
| + "mode, or all core properties must be defined in FULL mode."); |
| return; |
| } |
| // In FULL mode mOnOff is set to 'OFF', and can be changed by the CLUSTER_DISPLAY_STATE |
| // property. In LIGHT mode, we set it to 'ON' because the CLUSTER_DISPLAY_STATE property may |
| // not be available, and we do not subscribe to it. |
| if (mClusterHalService.isLightMode()) { |
| mOnOff = DISPLAY_ON; |
| } |
| |
| mServiceEnabled = true; |
| mClusterHalService.setCallback(this); |
| mClusterNavigationService.setClusterServiceCallback(this); |
| |
| mOccupantZoneService.registerCallback(mOccupantZoneCallback); |
| |
| if (mClusterHalService.isHeartbeatSupported()) { |
| mCarActivityService.registerActivityLaunchListener(mActivityLaunchListener); |
| } |
| initClusterDisplay(); |
| } |
| |
| private void initClusterDisplay() { |
| int clusterDisplayId = mOccupantZoneService.getDisplayIdForDriver( |
| CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER); |
| Slogf.d(TAG, "initClusterDisplay: displayId=%d", clusterDisplayId); |
| if (clusterDisplayId == Display.INVALID_DISPLAY) { |
| Slogf.i(TAG, "No cluster display is defined"); |
| } |
| if (clusterDisplayId == mClusterDisplayId) { |
| return; // Skip if the cluster display isn't changed. |
| } |
| mClusterDisplayId = clusterDisplayId; |
| sendDisplayState(ClusterHomeManager.CONFIG_DISPLAY_ID); |
| if (clusterDisplayId == Display.INVALID_DISPLAY) { |
| return; |
| } |
| |
| // Initialize mBounds only once. |
| if (mBounds.right == 0 && mBounds.bottom == 0 && mBounds.left == 0 && mBounds.top == 0) { |
| DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); |
| Display clusterDisplay = displayManager.getDisplay(clusterDisplayId); |
| Point size = new Point(); |
| clusterDisplay.getRealSize(size); |
| mBounds.right = size.x; |
| mBounds.bottom = size.y; |
| Slogf.d(TAG, "Found cluster displayId=%d, bounds=%s", clusterDisplayId, mBounds); |
| } |
| |
| ActivityOptions activityOptions = ActivityOptions.makeBasic() |
| .setLaunchDisplayId(clusterDisplayId); |
| mFixedActivityService.startFixedActivityModeForDisplayAndUser( |
| mLastIntent, activityOptions, clusterDisplayId, mLastIntentUserId); |
| } |
| |
| private final ICarOccupantZoneCallback mOccupantZoneCallback = |
| new ICarOccupantZoneCallback.Stub() { |
| @Override |
| public void onOccupantZoneConfigChanged(int flags) throws RemoteException { |
| if ((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0) { |
| initClusterDisplay(); |
| } |
| } |
| }; |
| |
| @Override |
| public void release() { |
| Slogf.d(TAG, "releaseClusterHomeService"); |
| if (mClusterHalService.isHeartbeatSupported()) { |
| mCarActivityService.unregisterActivityLaunchListener(mActivityLaunchListener); |
| } |
| mOccupantZoneService.unregisterCallback(mOccupantZoneCallback); |
| mClusterHalService.setCallback(null); |
| mClusterNavigationService.setClusterServiceCallback(null); |
| mClientListeners.kill(); |
| mClientNavigationListeners.kill(); |
| } |
| |
| private final CarActivityService.ActivityLaunchListener mActivityLaunchListener = |
| (topTask) -> { |
| if (TaskInfoHelper.getDisplayId(topTask) != mClusterDisplayId) return; |
| if (!mLastIntent.getComponent().equals(topTask.topActivity)) { |
| mClusterActivityVisible = false; |
| return; |
| } |
| |
| // TODO: b/285415531 - Install TPL and TPL decides the visibility. |
| mClusterActivityVisible = true; |
| }; |
| |
| @Override |
| @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) |
| public void dump(IndentingPrintWriter writer) { |
| // TODO: record the latest states from both sides |
| } |
| |
| @Override |
| @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) |
| public void dumpProto(ProtoOutputStream proto) {} |
| |
| // ClusterHalEventListener starts |
| @Override |
| public void onSwitchUi(int uiType) { |
| Slogf.d(TAG, "onSwitchUi: uiType=%d", uiType); |
| int changes = 0; |
| if (mUiType != uiType) { |
| mUiType = uiType; |
| changes |= ClusterHomeManager.CONFIG_UI_TYPE; |
| } |
| sendDisplayState(changes); |
| } |
| |
| @Override |
| public void onDisplayState(int onOff, Rect bounds, Insets insets) { |
| Slogf.d(TAG, "onDisplayState: onOff=%d, bounds=%s, insets=%s", onOff, bounds, insets); |
| int changes = 0; |
| if (onOff != DONT_CARE && mOnOff != onOff) { |
| mOnOff = onOff; |
| changes |= ClusterHomeManager.CONFIG_DISPLAY_ON_OFF; |
| } |
| if (bounds != null && !mBounds.equals(bounds)) { |
| mBounds = bounds; |
| changes |= ClusterHomeManager.CONFIG_DISPLAY_BOUNDS; |
| } |
| if (insets != null && !mInsets.equals(insets)) { |
| mInsets = insets; |
| changes |= ClusterHomeManager.CONFIG_DISPLAY_INSETS; |
| } |
| sendDisplayState(changes); |
| } |
| // ClusterHalEventListener ends |
| |
| private void sendDisplayState(int changes) { |
| ClusterState state = createClusterState(); |
| int n = mClientListeners.beginBroadcast(); |
| for (int i = 0; i < n; i++) { |
| IClusterStateListener callback = mClientListeners.getBroadcastItem(i); |
| try { |
| callback.onClusterStateChanged(state, changes); |
| } catch (RemoteException ignores) { |
| // ignore |
| } |
| } |
| mClientListeners.finishBroadcast(); |
| } |
| |
| // ClusterNavigationServiceCallback starts |
| @Override |
| public void onNavigationStateChanged(Bundle bundle) { |
| byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY); |
| |
| sendNavigationState(protoBytes); |
| } |
| |
| private void sendNavigationState(byte[] protoBytes) { |
| final int n = mClientNavigationListeners.beginBroadcast(); |
| for (int i = 0; i < n; i++) { |
| IClusterNavigationStateListener callback = |
| mClientNavigationListeners.getBroadcastItem(i); |
| try { |
| callback.onNavigationStateChanged(protoBytes); |
| } catch (RemoteException ignores) { |
| // ignore |
| } |
| } |
| mClientNavigationListeners.finishBroadcast(); |
| |
| if (!mClusterHalService.isNavigationStateSupported()) { |
| Slogf.d(TAG, "No Cluster NavigationState HAL property"); |
| return; |
| } |
| mClusterHalService.sendNavigationState(protoBytes); |
| } |
| |
| @Override |
| public CarNavigationInstrumentCluster getInstrumentClusterInfo() { |
| return CarNavigationInstrumentCluster.createCluster(DEFAULT_MIN_UPDATE_INTERVAL_MILLIS); |
| } |
| |
| @Override |
| public void notifyNavContextOwnerChanged(ClusterNavigationService.ContextOwner owner) { |
| Slogf.d(TAG, "notifyNavContextOwnerChanged: owner=%s", owner); |
| // Sends the empty NavigationStateProto to clear out the last direction |
| // when the app context owner is changed or the navigation is finished. |
| NavigationStateProto emptyProto = NavigationStateProto.newBuilder() |
| .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL).build(); |
| sendNavigationState(emptyProto.toByteArray()); |
| } |
| // ClusterNavigationServiceCallback ends |
| |
| // IClusterHomeService starts |
| @Override |
| public void reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability) { |
| Slogf.d(TAG, "reportState: main=%d, sub=%d", uiTypeMain, uiTypeSub); |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| |
| mUiType = uiTypeMain; |
| mClusterHalService.reportState(mOnOff, mBounds, mInsets, |
| uiTypeMain, uiTypeSub, uiAvailability); |
| } |
| |
| @Override |
| public void requestDisplay(int uiType) { |
| Slogf.d(TAG, "requestDisplay: uiType=%d", uiType); |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| |
| mClusterHalService.requestDisplay(uiType); |
| } |
| |
| @Override |
| public boolean startFixedActivityModeAsUser(Intent intent, |
| Bundle activityOptionsBundle, int userId) { |
| Slogf.d(TAG, "startFixedActivityModeAsUser: intent=%s, userId=%d", intent, userId); |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| if (mClusterDisplayId == Display.INVALID_DISPLAY) { |
| Slogf.e(TAG, "Cluster display is not ready."); |
| return false; |
| } |
| |
| ActivityOptions activityOptions = activityOptionsBundle != null |
| ? createActivityOptions(activityOptionsBundle) |
| : ActivityOptions.makeBasic(); |
| activityOptions.setLaunchDisplayId(mClusterDisplayId); |
| mLastIntent = intent; |
| mLastIntentUserId = userId; |
| return mFixedActivityService.startFixedActivityModeForDisplayAndUser( |
| intent, activityOptions, mClusterDisplayId, userId); |
| } |
| |
| @Override |
| public void stopFixedActivityMode() { |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| if (mClusterDisplayId == Display.INVALID_DISPLAY) { |
| Slogf.e(TAG, "Cluster display is not ready."); |
| return; |
| } |
| |
| mFixedActivityService.stopFixedActivityMode(mClusterDisplayId); |
| } |
| |
| @Override |
| public void registerClusterStateListener(IClusterStateListener listener) { |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| |
| mClientListeners.register(listener); |
| } |
| |
| @Override |
| public void unregisterClusterStateListener(IClusterStateListener listener) { |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| |
| mClientListeners.unregister(listener); |
| } |
| |
| @Override |
| public void registerClusterNavigationStateListener(IClusterNavigationStateListener listener) { |
| enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| |
| mClientNavigationListeners.register(listener); |
| } |
| |
| @Override |
| public void unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener) { |
| enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| |
| mClientNavigationListeners.unregister(listener); |
| } |
| |
| @Override |
| public ClusterState getClusterState() { |
| Slogf.d(TAG, "getClusterState"); |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); |
| return createClusterState(); |
| } |
| |
| @Override |
| public void sendHeartbeat(long epochTimeNs, byte[] appMetadata) { |
| enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); |
| if (appMetadata == null) { |
| appMetadata = EMPTY_BYTE_ARRAY; |
| } |
| mClusterHalService.sendHeartbeat(epochTimeNs, mClusterActivityVisible ? 1 : 0, appMetadata); |
| } |
| // IClusterHomeService ends |
| |
| private void enforcePermission(String permissionName) { |
| if (mContext.checkCallingOrSelfPermission(permissionName) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("requires permission " + permissionName); |
| } |
| } |
| |
| private ClusterState createClusterState() { |
| ClusterState state = new ClusterState(); |
| state.on = mOnOff == DISPLAY_ON; |
| state.bounds = mBounds; |
| state.insets = mInsets; |
| state.uiType = mUiType; |
| state.displayId = mClusterDisplayId; |
| return state; |
| } |
| } |