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 android.car.cluster; 18 19 import static android.car.feature.Flags.FLAG_CLUSTER_HEALTH_MONITORING; 20 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 21 22 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; 23 24 import android.annotation.FlaggedApi; 25 import android.annotation.IntDef; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.annotation.RequiresPermission; 29 import android.annotation.SystemApi; 30 import android.app.Activity; 31 import android.car.Car; 32 import android.car.CarManagerBase; 33 import android.car.builtin.util.Slogf; 34 import android.car.builtin.view.SurfaceControlHelper; 35 import android.content.Intent; 36 import android.os.Bundle; 37 import android.os.IBinder; 38 import android.os.RemoteException; 39 import android.view.SurfaceControl; 40 import android.view.ViewTreeObserver; 41 import android.view.ViewTreeObserver.OnPreDrawListener; 42 43 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 44 import com.android.car.internal.ICarBase; 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import java.lang.annotation.Retention; 48 import java.lang.annotation.RetentionPolicy; 49 import java.lang.ref.WeakReference; 50 import java.util.Objects; 51 import java.util.concurrent.CopyOnWriteArrayList; 52 import java.util.concurrent.Executor; 53 54 /** 55 * Provides the api to manage {@code ClusterHome}. 56 * 57 * @hide 58 */ 59 @FlaggedApi(FLAG_CLUSTER_HEALTH_MONITORING) 60 @SystemApi 61 public final class ClusterHomeManager extends CarManagerBase { 62 private static final String TAG = ClusterHomeManager.class.getSimpleName(); 63 /** 64 * When the client reports ClusterHome state and if there is no UI in the sub area, it can 65 * reports UI_TYPE_CLUSTER_NONE instead. 66 * 67 * @hide 68 */ 69 public static final int UI_TYPE_CLUSTER_NONE = -1; 70 /** @hide */ 71 public static final int UI_TYPE_CLUSTER_HOME = 0; 72 73 /** @hide */ 74 @IntDef(flag = true, prefix = { "CONFIG_" }, value = { 75 CONFIG_DISPLAY_ON_OFF, 76 CONFIG_DISPLAY_BOUNDS, 77 CONFIG_DISPLAY_INSETS, 78 CONFIG_UI_TYPE, 79 }) 80 @Retention(RetentionPolicy.SOURCE) 81 public @interface Config {} 82 83 /** Bit fields indicates which fields of {@link ClusterState} are changed */ 84 /** @hide */ 85 public static final int CONFIG_DISPLAY_ON_OFF = 0x01; 86 /** @hide */ 87 public static final int CONFIG_DISPLAY_BOUNDS = 0x02; 88 /** @hide */ 89 public static final int CONFIG_DISPLAY_INSETS = 0x04; 90 /** @hide */ 91 public static final int CONFIG_UI_TYPE = 0x08; 92 /** @hide */ 93 public static final int CONFIG_DISPLAY_ID = 0x10; 94 95 /** 96 * Callback for ClusterHome to get notifications when cluster state changes. 97 * 98 * @hide 99 */ 100 public interface ClusterStateListener { 101 /** 102 * Called when ClusterOS changes the cluster display state, the geometry of cluster display, 103 * or the uiType. 104 * @param state newly updated {@link ClusterState} 105 * @param changes the flag indicates which fields are updated 106 */ onClusterStateChanged(ClusterState state, @Config int changes)107 void onClusterStateChanged(ClusterState state, @Config int changes); 108 } 109 110 /** 111 * Callback for ClusterHome to get notifications when cluster navigation state changes. 112 */ 113 @FlaggedApi(FLAG_CLUSTER_HEALTH_MONITORING) 114 public interface ClusterNavigationStateListener { 115 /** 116 * Called when the app who owns the navigation focus casts the new navigation state. 117 * 118 * @param navigationState Byte array that is serialized from a {@link 119 * android.car.cluster.navigation.NavigationState.NavigationStateProto} proto value. 120 */ onNavigationStateChanged(@onNull byte[] navigationState)121 void onNavigationStateChanged(@NonNull byte[] navigationState); 122 } 123 124 private static class ClusterStateListenerRecord { 125 final Executor mExecutor; 126 final ClusterStateListener mListener; ClusterStateListenerRecord(Executor executor, ClusterStateListener listener)127 ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) { 128 mExecutor = executor; 129 mListener = listener; 130 } 131 132 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 133 @Override equals(Object obj)134 public boolean equals(Object obj) { 135 if (this == obj) { 136 return true; 137 } 138 if (!(obj instanceof ClusterStateListenerRecord)) { 139 return false; 140 } 141 return mListener == ((ClusterStateListenerRecord) obj).mListener; 142 } 143 144 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 145 @Override hashCode()146 public int hashCode() { 147 return mListener.hashCode(); 148 } 149 } 150 151 private static class ClusterNavigationStateListenerRecord { 152 final Executor mExecutor; 153 final ClusterNavigationStateListener mListener; 154 ClusterNavigationStateListenerRecord(Executor executor, ClusterNavigationStateListener listener)155 ClusterNavigationStateListenerRecord(Executor executor, 156 ClusterNavigationStateListener listener) { 157 mExecutor = executor; 158 mListener = listener; 159 } 160 161 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 162 @Override equals(Object obj)163 public boolean equals(Object obj) { 164 if (this == obj) { 165 return true; 166 } 167 if (!(obj instanceof ClusterNavigationStateListenerRecord)) { 168 return false; 169 } 170 return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener; 171 } 172 173 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 174 @Override hashCode()175 public int hashCode() { 176 return mListener.hashCode(); 177 } 178 } 179 180 private final IClusterHomeService mService; 181 private final IClusterStateListenerImpl mClusterStateListenerBinderCallback; 182 private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback; 183 private final CopyOnWriteArrayList<ClusterStateListenerRecord> mStateListeners = 184 new CopyOnWriteArrayList<>(); 185 private final CopyOnWriteArrayList<ClusterNavigationStateListenerRecord> 186 mNavigationStateListeners = new CopyOnWriteArrayList<>(); 187 188 private boolean mVisibilityMonitoringStarted = false; 189 190 /** @hide */ 191 @VisibleForTesting ClusterHomeManager(ICarBase car, IBinder service)192 public ClusterHomeManager(ICarBase car, IBinder service) { 193 super(car); 194 mService = IClusterHomeService.Stub.asInterface(service); 195 mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this); 196 mClusterNavigationStateListenerBinderCallback = 197 new IClusterNavigationStateListenerImpl(this); 198 } 199 200 /** 201 * Registers the callback for ClusterHome. 202 * 203 * @hide 204 */ 205 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) registerClusterStateListener( @onNull Executor executor, @NonNull ClusterStateListener callback)206 public void registerClusterStateListener( 207 @NonNull Executor executor, @NonNull ClusterStateListener callback) { 208 Objects.requireNonNull(executor, "executor cannot be null"); 209 Objects.requireNonNull(callback, "callback cannot be null"); 210 ClusterStateListenerRecord clusterStateListenerRecord = 211 new ClusterStateListenerRecord(executor, callback); 212 if (!mStateListeners.addIfAbsent(clusterStateListenerRecord)) { 213 return; 214 } 215 if (mStateListeners.size() == 1) { 216 try { 217 mService.registerClusterStateListener(mClusterStateListenerBinderCallback); 218 } catch (RemoteException e) { 219 handleRemoteExceptionFromCarService(e); 220 } 221 } 222 } 223 224 /** 225 * Registers a listener for navigation state changes. 226 * 227 * <p>Note that multiple listeners can be registered. All registered listeners are invoked 228 * when the navigation app that has the focus sends a state change. 229 * <p>A listener is invoked only for changes that occur after the registration. It is not 230 * called for the previous or current states at the time of the registration. 231 */ 232 @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE) registerClusterNavigationStateListener( @onNull Executor executor, @NonNull ClusterNavigationStateListener callback)233 public void registerClusterNavigationStateListener( 234 @NonNull Executor executor, @NonNull ClusterNavigationStateListener callback) { 235 Objects.requireNonNull(executor, "executor cannot be null"); 236 Objects.requireNonNull(callback, "callback cannot be null"); 237 ClusterNavigationStateListenerRecord clusterStateListenerRecord = 238 new ClusterNavigationStateListenerRecord(executor, callback); 239 if (!mNavigationStateListeners.addIfAbsent(clusterStateListenerRecord)) { 240 return; 241 } 242 if (mNavigationStateListeners.size() == 1) { 243 try { 244 mService.registerClusterNavigationStateListener( 245 mClusterNavigationStateListenerBinderCallback); 246 } catch (RemoteException e) { 247 handleRemoteExceptionFromCarService(e); 248 } 249 } 250 } 251 252 /** 253 * Unregisters the callback. 254 * 255 * @hide 256 */ 257 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) unregisterClusterStateListener(@onNull ClusterStateListener callback)258 public void unregisterClusterStateListener(@NonNull ClusterStateListener callback) { 259 Objects.requireNonNull(callback, "callback cannot be null"); 260 if (!mStateListeners 261 .remove(new ClusterStateListenerRecord(/* executor= */ null, callback))) { 262 return; 263 } 264 if (mStateListeners.isEmpty()) { 265 try { 266 mService.unregisterClusterStateListener(mClusterStateListenerBinderCallback); 267 } catch (RemoteException ignored) { 268 // ignore for unregistering 269 } 270 } 271 } 272 273 /** 274 * Unregisters a listener for navigation state changes. 275 */ 276 @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE) unregisterClusterNavigationStateListener( @onNull ClusterNavigationStateListener callback)277 public void unregisterClusterNavigationStateListener( 278 @NonNull ClusterNavigationStateListener callback) { 279 Objects.requireNonNull(callback, "callback cannot be null"); 280 if (!mNavigationStateListeners.remove(new ClusterNavigationStateListenerRecord( 281 /* executor= */ null, callback))) { 282 return; 283 } 284 if (mNavigationStateListeners.isEmpty()) { 285 try { 286 mService.unregisterClusterNavigationStateListener( 287 mClusterNavigationStateListenerBinderCallback); 288 } catch (RemoteException ignored) { 289 // ignore for unregistering 290 } 291 } 292 } 293 294 private static class IClusterStateListenerImpl extends IClusterStateListener.Stub { 295 private final WeakReference<ClusterHomeManager> mManager; 296 IClusterStateListenerImpl(ClusterHomeManager manager)297 private IClusterStateListenerImpl(ClusterHomeManager manager) { 298 mManager = new WeakReference<>(manager); 299 } 300 301 @Override onClusterStateChanged(@onNull ClusterState state, @Config int changes)302 public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) { 303 ClusterHomeManager manager = mManager.get(); 304 if (manager != null) { 305 for (ClusterStateListenerRecord cb : manager.mStateListeners) { 306 cb.mExecutor.execute( 307 () -> cb.mListener.onClusterStateChanged(state, changes)); 308 } 309 } 310 } 311 } 312 313 private static class IClusterNavigationStateListenerImpl extends 314 IClusterNavigationStateListener.Stub { 315 private final WeakReference<ClusterHomeManager> mManager; 316 IClusterNavigationStateListenerImpl(ClusterHomeManager manager)317 private IClusterNavigationStateListenerImpl(ClusterHomeManager manager) { 318 mManager = new WeakReference<>(manager); 319 } 320 321 @Override onNavigationStateChanged(@onNull byte[] navigationState)322 public void onNavigationStateChanged(@NonNull byte[] navigationState) { 323 ClusterHomeManager manager = mManager.get(); 324 if (manager != null) { 325 for (ClusterNavigationStateListenerRecord lr : manager.mNavigationStateListeners) { 326 lr.mExecutor.execute( 327 () -> lr.mListener.onNavigationStateChanged(navigationState)); 328 } 329 } 330 } 331 } 332 333 /** 334 * Reports the current ClusterUI state. 335 * @param uiTypeMain uiType that ClusterHome tries to show in main area 336 * @param uiTypeSub uiType that ClusterHome tries to show in sub area 337 * @param uiAvailability the byte array to represent the availability of ClusterUI. 338 * 0 indicates non-available and 1 indicates available. 339 * Index 0 is reserved for ClusterHome, The other indexes are followed by OEM's definition. 340 * 341 * @hide 342 */ 343 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability)344 public void reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability) { 345 try { 346 mService.reportState(uiTypeMain, uiTypeSub, uiAvailability); 347 } catch (RemoteException e) { 348 handleRemoteExceptionFromCarService(e); 349 } 350 } 351 352 /** 353 * Requests to turn the cluster display on to show some ClusterUI. 354 * @param uiType uiType that ClusterHome tries to show in main area 355 * 356 * @hide 357 */ 358 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) requestDisplay(int uiType)359 public void requestDisplay(int uiType) { 360 try { 361 mService.requestDisplay(uiType); 362 } catch (RemoteException e) { 363 handleRemoteExceptionFromCarService(e); 364 } 365 } 366 367 /** 368 * Returns the current {@code ClusterState}. 369 * 370 * @hide 371 */ 372 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 373 @Nullable getClusterState()374 public ClusterState getClusterState() { 375 ClusterState state = null; 376 try { 377 state = mService.getClusterState(); 378 } catch (RemoteException e) { 379 handleRemoteExceptionFromCarService(e); 380 } 381 return state; 382 } 383 384 /** 385 * Start an activity as specified user. The activity is considered as in fixed mode for 386 * the cluster display and will be re-launched if the activity crashes, the package 387 * is updated or goes to background for whatever reason. 388 * Only one activity can exist in fixed mode for the display and calling this multiple 389 * times with different {@code Intent} will lead into making all previous activities into 390 * non-fixed normal state (= will not be re-launched.) 391 * @param intent the Intent to start 392 * @param options additional options for how the Activity should be started 393 * @param userId the user the new activity should run as 394 * @return true if it launches the given Intent as FixedActivity successfully 395 * 396 * @hide 397 */ 398 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) startFixedActivityModeAsUser( Intent intent, @Nullable Bundle options, int userId)399 public boolean startFixedActivityModeAsUser( 400 Intent intent, @Nullable Bundle options, int userId) { 401 try { 402 return mService.startFixedActivityModeAsUser(intent, options, userId); 403 } catch (RemoteException e) { 404 handleRemoteExceptionFromCarService(e); 405 } 406 return false; 407 } 408 409 /** 410 * The activity launched on the cluster display is no longer in fixed mode. Re-launching or 411 * finishing should not trigger re-launching any more. Note that Activity for non-current user 412 * will be auto-stopped and there is no need to call this for user switching. Note that this 413 * does not stop the activity but it will not be re-launched any more. 414 * 415 * @hide 416 */ 417 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) stopFixedActivityMode()418 public void stopFixedActivityMode() { 419 try { 420 mService.stopFixedActivityMode(); 421 } catch (RemoteException e) { 422 handleRemoteExceptionFromCarService(e); 423 } 424 } 425 426 /** 427 * Sends a heartbeat to ClusterOS. 428 * @param epochTimeNs the current time 429 * @param appMetadata the application specific metadata which will be delivered with 430 * the heartbeat. 431 */ 432 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) sendHeartbeat(long epochTimeNs, @Nullable byte[] appMetadata)433 public void sendHeartbeat(long epochTimeNs, @Nullable byte[] appMetadata) { 434 try { 435 mService.sendHeartbeat(epochTimeNs, appMetadata); 436 } catch (RemoteException e) { 437 handleRemoteExceptionFromCarService(e); 438 } 439 } 440 441 /** 442 * Starts the visibility monitoring of given {@link Activity}. 443 * 444 * Note: This is supposed to be called in {@link Activity#onStart()} generally. 445 * 446 * @param activity the {@link Activity} to track the visibility of its Window 447 */ 448 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) startVisibilityMonitoring(@onNull Activity activity)449 public void startVisibilityMonitoring(@NonNull Activity activity) { 450 // We'd like to check the permission locally too, since the actual execution happens later. 451 if (getContext().checkCallingOrSelfPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 452 != PERMISSION_GRANTED) { 453 throw new SecurityException( 454 "requires permission " + Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL); 455 } 456 if (mVisibilityMonitoringStarted) { 457 Slogf.w(TAG, "startVisibilityMonitoring is already started"); 458 return; 459 } 460 if (SurfaceControlHelper.getSurfaceControl(activity) != null) { 461 throw new IllegalStateException( 462 "startVisibilityMonitoring is expected to be called before onAttachedToWindow"); 463 } 464 mVisibilityMonitoringStarted = true; 465 ViewTreeObserver observer = getViewTreeObserver(activity); 466 // Can't use onWindowAttached, because SurfaceControl is available at the time, but invalid. 467 // TODO: b/286406553 - Move the callback below to onWindowAttached. 468 observer.addOnPreDrawListener( 469 new OnPreDrawListener() { 470 @Override 471 public boolean onPreDraw() { 472 // The existing 'observer' would be invalid, so gets it again. 473 ViewTreeObserver observer = getViewTreeObserver(activity); 474 observer.removeOnPreDrawListener(this); 475 startVisibilityMonitoringInternal(activity); 476 return true; 477 } 478 }); 479 observer.addOnWindowAttachListener( 480 new ViewTreeObserver.OnWindowAttachListener() { 481 @Override 482 public void onWindowAttached() { 483 // Using onPreDraw instead, check b/286406553. 484 } 485 486 @Override 487 public void onWindowDetached() { 488 ViewTreeObserver observer = getViewTreeObserver(activity); 489 observer.removeOnWindowAttachListener(this); 490 stopVisibilityMonitoringInternal(); 491 } 492 } 493 ); 494 } 495 getViewTreeObserver(@onNull Activity activity)496 private static ViewTreeObserver getViewTreeObserver(@NonNull Activity activity) { 497 return activity.getWindow().getDecorView().getViewTreeObserver(); 498 } 499 startVisibilityMonitoringInternal(Activity activity)500 private void startVisibilityMonitoringInternal(Activity activity) { 501 SurfaceControl surfaceControl = SurfaceControlHelper.getSurfaceControl(activity); 502 try { 503 mService.startVisibilityMonitoring(surfaceControl); 504 } catch (RemoteException e) { 505 Slogf.e(TAG, "Failed to startVisibilityMonitoring", e); 506 } 507 } 508 stopVisibilityMonitoringInternal()509 private void stopVisibilityMonitoringInternal() { 510 try { 511 mService.stopVisibilityMonitoring(); 512 mVisibilityMonitoringStarted = false; 513 } catch (RemoteException e) { 514 Slogf.e(TAG, "Failed to stopVisibilityMonitoring", e); 515 } 516 } 517 518 /** @hide */ 519 @Override onCarDisconnected()520 protected void onCarDisconnected() { 521 mStateListeners.clear(); 522 mNavigationStateListeners.clear(); 523 } 524 } 525