blob: a9ef03184ca1d6be6f5a8867b60e7ec367fb4281 [file] [log] [blame]
/*
* 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;
}
}