1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.cluster; 18 19 import static android.content.Intent.ACTION_MAIN; 20 21 import static com.android.car.hal.ClusterHalService.DISPLAY_OFF; 22 import static com.android.car.hal.ClusterHalService.DISPLAY_ON; 23 import static com.android.car.hal.ClusterHalService.DONT_CARE; 24 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 25 26 import android.app.ActivityOptions; 27 import android.car.Car; 28 import android.car.CarOccupantZoneManager; 29 import android.car.ICarOccupantZoneCallback; 30 import android.car.cluster.ClusterHomeManager; 31 import android.car.cluster.ClusterState; 32 import android.car.cluster.IClusterHomeService; 33 import android.car.cluster.IClusterNavigationStateListener; 34 import android.car.cluster.IClusterStateListener; 35 import android.car.cluster.navigation.NavigationState.NavigationStateProto; 36 import android.car.navigation.CarNavigationInstrumentCluster; 37 import android.content.ComponentName; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.pm.PackageManager; 41 import android.graphics.Insets; 42 import android.graphics.Point; 43 import android.graphics.Rect; 44 import android.hardware.display.DisplayManager; 45 import android.os.Bundle; 46 import android.os.RemoteCallbackList; 47 import android.os.RemoteException; 48 import android.os.UserHandle; 49 import android.text.TextUtils; 50 import android.util.IndentingPrintWriter; 51 import android.view.Display; 52 53 import com.android.car.CarLog; 54 import com.android.car.CarOccupantZoneService; 55 import com.android.car.CarServiceBase; 56 import com.android.car.R; 57 import com.android.car.am.FixedActivityService; 58 import com.android.car.hal.ClusterHalService; 59 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 60 import com.android.server.utils.Slogf; 61 62 /** 63 * Service responsible for interactions between ClusterOS and ClusterHome. 64 */ 65 public class ClusterHomeService extends IClusterHomeService.Stub 66 implements CarServiceBase, ClusterNavigationService.ClusterNavigationServiceCallback, 67 ClusterHalService.ClusterHalEventCallback { 68 private static final String TAG = CarLog.TAG_CLUSTER; 69 private static final int DEFAULT_MIN_UPDATE_INTERVAL_MILLIS = 1000; 70 private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2"; 71 72 private final Context mContext; 73 private final ClusterHalService mClusterHalService; 74 private final ClusterNavigationService mClusterNavigationService; 75 private final CarOccupantZoneService mOccupantZoneService; 76 private final FixedActivityService mFixedActivityService; 77 private final ComponentName mClusterHomeActivity; 78 79 private boolean mServiceEnabled; 80 81 private int mClusterDisplayId = Display.INVALID_DISPLAY; 82 83 private int mOnOff = DISPLAY_OFF; 84 private Rect mBounds = new Rect(); 85 private Insets mInsets = Insets.NONE; 86 private int mUiType = ClusterHomeManager.UI_TYPE_CLUSTER_HOME; 87 private Intent mLastIntent; 88 private int mLastIntentUserId = UserHandle.USER_SYSTEM; 89 90 private final RemoteCallbackList<IClusterStateListener> mClientListeners = 91 new RemoteCallbackList<>(); 92 93 private final RemoteCallbackList<IClusterNavigationStateListener> mClientNavigationListeners = 94 new RemoteCallbackList<>(); 95 ClusterHomeService(Context context, ClusterHalService clusterHalService, ClusterNavigationService navigationService, CarOccupantZoneService occupantZoneService, FixedActivityService fixedActivityService)96 public ClusterHomeService(Context context, ClusterHalService clusterHalService, 97 ClusterNavigationService navigationService, 98 CarOccupantZoneService occupantZoneService, 99 FixedActivityService fixedActivityService) { 100 mContext = context; 101 mClusterHalService = clusterHalService; 102 mClusterNavigationService = navigationService; 103 mOccupantZoneService = occupantZoneService; 104 mFixedActivityService = fixedActivityService; 105 mClusterHomeActivity = ComponentName.unflattenFromString( 106 mContext.getString(R.string.config_clusterHomeActivity)); 107 mLastIntent = new Intent(ACTION_MAIN).setComponent(mClusterHomeActivity); 108 } 109 110 @Override init()111 public void init() { 112 Slogf.d(TAG, "initClusterHomeService"); 113 if (TextUtils.isEmpty(mClusterHomeActivity.getPackageName()) 114 || TextUtils.isEmpty(mClusterHomeActivity.getClassName())) { 115 Slogf.i(TAG, "Improper ClusterHomeActivity: %s", mClusterHomeActivity); 116 return; 117 } 118 if (!mClusterHalService.isCoreSupported()) { 119 Slogf.e(TAG, "No Cluster HAL properties"); 120 return; 121 } 122 123 mServiceEnabled = true; 124 mClusterHalService.setCallback(this); 125 mClusterNavigationService.setClusterServiceCallback(this); 126 127 mOccupantZoneService.registerCallback(mOccupantZoneCallback); 128 initClusterDisplay(); 129 } 130 initClusterDisplay()131 private void initClusterDisplay() { 132 int clusterDisplayId = mOccupantZoneService.getDisplayIdForDriver( 133 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER); 134 Slogf.d(TAG, "initClusterDisplay: displayId=%d", clusterDisplayId); 135 if (clusterDisplayId == Display.INVALID_DISPLAY) { 136 Slogf.i(TAG, "No cluster display is defined"); 137 } 138 if (clusterDisplayId == mClusterDisplayId) { 139 return; // Skip if the cluster display isn't changed. 140 } 141 mClusterDisplayId = clusterDisplayId; 142 sendDisplayState(ClusterHomeManager.CONFIG_DISPLAY_ID); 143 if (clusterDisplayId == Display.INVALID_DISPLAY) { 144 return; 145 } 146 147 // Initialize mBounds only once. 148 if (mBounds.right == 0 && mBounds.bottom == 0 && mBounds.left == 0 && mBounds.top == 0) { 149 DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); 150 Display clusterDisplay = displayManager.getDisplay(clusterDisplayId); 151 Point size = new Point(); 152 clusterDisplay.getRealSize(size); 153 mBounds.right = size.x; 154 mBounds.bottom = size.y; 155 Slogf.d(TAG, "Found cluster displayId=%d, bounds=%s", clusterDisplayId, mBounds); 156 } 157 158 ActivityOptions activityOptions = ActivityOptions.makeBasic() 159 .setLaunchDisplayId(clusterDisplayId); 160 mFixedActivityService.startFixedActivityModeForDisplayAndUser( 161 mLastIntent, activityOptions, clusterDisplayId, mLastIntentUserId); 162 } 163 164 private final ICarOccupantZoneCallback mOccupantZoneCallback = 165 new ICarOccupantZoneCallback.Stub() { 166 @Override 167 public void onOccupantZoneConfigChanged(int flags) throws RemoteException { 168 if ((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0) { 169 initClusterDisplay(); 170 } 171 } 172 }; 173 174 @Override release()175 public void release() { 176 Slogf.d(TAG, "releaseClusterHomeService"); 177 mOccupantZoneService.unregisterCallback(mOccupantZoneCallback); 178 mClusterHalService.setCallback(null); 179 mClusterNavigationService.setClusterServiceCallback(null); 180 mClientListeners.kill(); 181 mClientNavigationListeners.kill(); 182 } 183 184 @Override 185 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)186 public void dump(IndentingPrintWriter writer) { 187 // TODO: record the latest states from both sides 188 } 189 190 // ClusterHalEventListener starts 191 @Override onSwitchUi(int uiType)192 public void onSwitchUi(int uiType) { 193 Slogf.d(TAG, "onSwitchUi: uiType=%d", uiType); 194 int changes = 0; 195 if (mUiType != uiType) { 196 mUiType = uiType; 197 changes |= ClusterHomeManager.CONFIG_UI_TYPE; 198 } 199 sendDisplayState(changes); 200 } 201 202 @Override onDisplayState(int onOff, Rect bounds, Insets insets)203 public void onDisplayState(int onOff, Rect bounds, Insets insets) { 204 Slogf.d(TAG, "onDisplayState: onOff=%d, bounds=%s, insets=%s", onOff, bounds, insets); 205 int changes = 0; 206 if (onOff != DONT_CARE && mOnOff != onOff) { 207 mOnOff = onOff; 208 changes |= ClusterHomeManager.CONFIG_DISPLAY_ON_OFF; 209 } 210 if (bounds != null && !mBounds.equals(bounds)) { 211 mBounds = bounds; 212 changes |= ClusterHomeManager.CONFIG_DISPLAY_BOUNDS; 213 } 214 if (insets != null && !mInsets.equals(insets)) { 215 mInsets = insets; 216 changes |= ClusterHomeManager.CONFIG_DISPLAY_INSETS; 217 } 218 sendDisplayState(changes); 219 } 220 // ClusterHalEventListener ends 221 sendDisplayState(int changes)222 private void sendDisplayState(int changes) { 223 ClusterState state = createClusterState(); 224 int n = mClientListeners.beginBroadcast(); 225 for (int i = 0; i < n; i++) { 226 IClusterStateListener callback = mClientListeners.getBroadcastItem(i); 227 try { 228 callback.onClusterStateChanged(state, changes); 229 } catch (RemoteException ignores) { 230 // ignore 231 } 232 } 233 mClientListeners.finishBroadcast(); 234 } 235 236 // ClusterNavigationServiceCallback starts 237 @Override onNavigationStateChanged(Bundle bundle)238 public void onNavigationStateChanged(Bundle bundle) { 239 byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY); 240 241 sendNavigationState(protoBytes); 242 } 243 sendNavigationState(byte[] protoBytes)244 private void sendNavigationState(byte[] protoBytes) { 245 final int n = mClientNavigationListeners.beginBroadcast(); 246 for (int i = 0; i < n; i++) { 247 IClusterNavigationStateListener callback = 248 mClientNavigationListeners.getBroadcastItem(i); 249 try { 250 callback.onNavigationStateChanged(protoBytes); 251 } catch (RemoteException ignores) { 252 // ignore 253 } 254 } 255 mClientNavigationListeners.finishBroadcast(); 256 257 if (!mClusterHalService.isNavigationStateSupported()) { 258 Slogf.d(TAG, "No Cluster NavigationState HAL property"); 259 return; 260 } 261 mClusterHalService.sendNavigationState(protoBytes); 262 } 263 264 @Override getInstrumentClusterInfo()265 public CarNavigationInstrumentCluster getInstrumentClusterInfo() { 266 return CarNavigationInstrumentCluster.createCluster(DEFAULT_MIN_UPDATE_INTERVAL_MILLIS); 267 } 268 269 @Override notifyNavContextOwnerChanged(ClusterNavigationService.ContextOwner owner)270 public void notifyNavContextOwnerChanged(ClusterNavigationService.ContextOwner owner) { 271 Slogf.d(TAG, "notifyNavContextOwnerChanged: owner=%s", owner); 272 // Sends the empty NavigationStateProto to clear out the last direction 273 // when the app context owner is changed or the navigation is finished. 274 NavigationStateProto emptyProto = NavigationStateProto.newBuilder() 275 .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL).build(); 276 sendNavigationState(emptyProto.toByteArray()); 277 } 278 // ClusterNavigationServiceCallback ends 279 280 // IClusterHomeService starts 281 @Override reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability)282 public void reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability) { 283 Slogf.d(TAG, "reportState: main=%d, sub=%d", uiTypeMain, uiTypeSub); 284 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 285 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 286 287 mClusterHalService.reportState(mOnOff, mBounds, mInsets, 288 uiTypeMain, uiTypeSub, uiAvailability); 289 } 290 291 @Override requestDisplay(int uiType)292 public void requestDisplay(int uiType) { 293 Slogf.d(TAG, "requestDisplay: uiType=%d", uiType); 294 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 295 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 296 297 mClusterHalService.requestDisplay(uiType); 298 } 299 300 @Override startFixedActivityModeAsUser(Intent intent, Bundle activityOptionsBundle, int userId)301 public boolean startFixedActivityModeAsUser(Intent intent, 302 Bundle activityOptionsBundle, int userId) { 303 Slogf.d(TAG, "startFixedActivityModeAsUser: intent=%s, userId=%d", intent, userId); 304 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 305 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 306 if (mClusterDisplayId == Display.INVALID_DISPLAY) { 307 Slogf.e(TAG, "Cluster display is not ready."); 308 return false; 309 } 310 311 ActivityOptions activityOptions = ActivityOptions.fromBundle(activityOptionsBundle); 312 activityOptions.setLaunchDisplayId(mClusterDisplayId); 313 mLastIntent = intent; 314 mLastIntentUserId = userId; 315 return mFixedActivityService.startFixedActivityModeForDisplayAndUser( 316 intent, activityOptions, mClusterDisplayId, userId); 317 } 318 319 @Override stopFixedActivityMode()320 public void stopFixedActivityMode() { 321 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 322 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 323 if (mClusterDisplayId == Display.INVALID_DISPLAY) { 324 Slogf.e(TAG, "Cluster display is not ready."); 325 return; 326 } 327 328 mFixedActivityService.stopFixedActivityMode(mClusterDisplayId); 329 } 330 331 @Override registerClusterStateListener(IClusterStateListener listener)332 public void registerClusterStateListener(IClusterStateListener listener) { 333 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 334 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 335 336 mClientListeners.register(listener); 337 } 338 339 @Override unregisterClusterStateListener(IClusterStateListener listener)340 public void unregisterClusterStateListener(IClusterStateListener listener) { 341 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 342 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 343 344 mClientListeners.unregister(listener); 345 } 346 347 @Override registerClusterNavigationStateListener(IClusterNavigationStateListener listener)348 public void registerClusterNavigationStateListener(IClusterNavigationStateListener listener) { 349 enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE); 350 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 351 352 mClientNavigationListeners.register(listener); 353 } 354 355 @Override unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener)356 public void unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener) { 357 enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE); 358 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 359 360 mClientNavigationListeners.unregister(listener); 361 } 362 363 @Override getClusterState()364 public ClusterState getClusterState() { 365 Slogf.d(TAG, "getClusterState"); 366 enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 367 if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled"); 368 return createClusterState(); 369 } 370 // IClusterHomeService ends 371 enforcePermission(String permissionName)372 private void enforcePermission(String permissionName) { 373 if (mContext.checkCallingOrSelfPermission(permissionName) 374 != PackageManager.PERMISSION_GRANTED) { 375 throw new SecurityException("requires permission " + permissionName); 376 } 377 } 378 createClusterState()379 private ClusterState createClusterState() { 380 ClusterState state = new ClusterState(); 381 state.on = mOnOff == DISPLAY_ON; 382 state.bounds = mBounds; 383 state.insets = mInsets; 384 state.uiType = mUiType; 385 state.displayId = mClusterDisplayId; 386 return state; 387 } 388 } 389