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 com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.car.Car; 26 import android.car.CarManagerBase; 27 import android.car.annotation.AddedInOrBefore; 28 import android.content.Intent; 29 import android.os.Bundle; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 33 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 34 import com.android.internal.annotations.VisibleForTesting; 35 36 import java.lang.annotation.Retention; 37 import java.lang.annotation.RetentionPolicy; 38 import java.lang.ref.WeakReference; 39 import java.util.Objects; 40 import java.util.concurrent.CopyOnWriteArrayList; 41 import java.util.concurrent.Executor; 42 43 /** @hide */ 44 public class ClusterHomeManager extends CarManagerBase { 45 private static final String TAG = ClusterHomeManager.class.getSimpleName(); 46 /** 47 * When the client reports ClusterHome state and if there is no UI in the sub area, it can 48 * reports UI_TYPE_CLUSTER_NONE instead. 49 */ 50 @AddedInOrBefore(majorVersion = 33) 51 public static final int UI_TYPE_CLUSTER_NONE = -1; 52 @AddedInOrBefore(majorVersion = 33) 53 public static final int UI_TYPE_CLUSTER_HOME = 0; 54 55 /** @hide */ 56 @IntDef(flag = true, prefix = { "CONFIG_" }, value = { 57 CONFIG_DISPLAY_ON_OFF, 58 CONFIG_DISPLAY_BOUNDS, 59 CONFIG_DISPLAY_INSETS, 60 CONFIG_UI_TYPE, 61 }) 62 @Retention(RetentionPolicy.SOURCE) 63 public @interface Config {} 64 65 /** Bit fields indicates which fields of {@link ClusterState} are changed */ 66 @AddedInOrBefore(majorVersion = 33) 67 public static final int CONFIG_DISPLAY_ON_OFF = 0x01; 68 @AddedInOrBefore(majorVersion = 33) 69 public static final int CONFIG_DISPLAY_BOUNDS = 0x02; 70 @AddedInOrBefore(majorVersion = 33) 71 public static final int CONFIG_DISPLAY_INSETS = 0x04; 72 @AddedInOrBefore(majorVersion = 33) 73 public static final int CONFIG_UI_TYPE = 0x08; 74 @AddedInOrBefore(majorVersion = 33) 75 public static final int CONFIG_DISPLAY_ID = 0x10; 76 77 /** 78 * Callback for ClusterHome to get notifications when cluster state changes. 79 */ 80 public interface ClusterStateListener { 81 /** 82 * Called when ClusterOS changes the cluster display state, the geometry of cluster display, 83 * or the uiType. 84 * @param state newly updated {@link ClusterState} 85 * @param changes the flag indicates which fields are updated 86 */ 87 @AddedInOrBefore(majorVersion = 33) onClusterStateChanged(ClusterState state, @Config int changes)88 void onClusterStateChanged(ClusterState state, @Config int changes); 89 } 90 91 /** 92 * Callback for ClusterHome to get notifications when cluster navigation state changes. 93 */ 94 public interface ClusterNavigationStateListener { 95 /** Called when the App who owns the navigation focus casts the new navigation state. */ 96 @AddedInOrBefore(majorVersion = 33) onNavigationState(byte[] navigationState)97 void onNavigationState(byte[] navigationState); 98 } 99 100 private static class ClusterStateListenerRecord { 101 final Executor mExecutor; 102 final ClusterStateListener mListener; ClusterStateListenerRecord(Executor executor, ClusterStateListener listener)103 ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) { 104 mExecutor = executor; 105 mListener = listener; 106 } 107 108 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 109 @Override equals(Object obj)110 public boolean equals(Object obj) { 111 if (this == obj) { 112 return true; 113 } 114 if (!(obj instanceof ClusterStateListenerRecord)) { 115 return false; 116 } 117 return mListener == ((ClusterStateListenerRecord) obj).mListener; 118 } 119 120 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 121 @Override hashCode()122 public int hashCode() { 123 return mListener.hashCode(); 124 } 125 } 126 127 private static class ClusterNavigationStateListenerRecord { 128 final Executor mExecutor; 129 final ClusterNavigationStateListener mListener; 130 ClusterNavigationStateListenerRecord(Executor executor, ClusterNavigationStateListener listener)131 ClusterNavigationStateListenerRecord(Executor executor, 132 ClusterNavigationStateListener listener) { 133 mExecutor = executor; 134 mListener = listener; 135 } 136 137 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 138 @Override equals(Object obj)139 public boolean equals(Object obj) { 140 if (this == obj) { 141 return true; 142 } 143 if (!(obj instanceof ClusterNavigationStateListenerRecord)) { 144 return false; 145 } 146 return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener; 147 } 148 149 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) 150 @Override hashCode()151 public int hashCode() { 152 return mListener.hashCode(); 153 } 154 } 155 156 private final IClusterHomeService mService; 157 private final IClusterStateListenerImpl mClusterStateListenerBinderCallback; 158 private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback; 159 private final CopyOnWriteArrayList<ClusterStateListenerRecord> mStateListeners = 160 new CopyOnWriteArrayList<>(); 161 private final CopyOnWriteArrayList<ClusterNavigationStateListenerRecord> 162 mNavigationStateListeners = new CopyOnWriteArrayList<>(); 163 164 /** @hide */ 165 @VisibleForTesting ClusterHomeManager(Car car, IBinder service)166 public ClusterHomeManager(Car car, IBinder service) { 167 super(car); 168 mService = IClusterHomeService.Stub.asInterface(service); 169 mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this); 170 mClusterNavigationStateListenerBinderCallback = 171 new IClusterNavigationStateListenerImpl(this); 172 } 173 174 /** 175 * Registers the callback for ClusterHome. 176 */ 177 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 178 @AddedInOrBefore(majorVersion = 33) registerClusterStateListener( @onNull Executor executor, @NonNull ClusterStateListener callback)179 public void registerClusterStateListener( 180 @NonNull Executor executor, @NonNull ClusterStateListener callback) { 181 Objects.requireNonNull(executor, "executor cannot be null"); 182 Objects.requireNonNull(callback, "callback cannot be null"); 183 ClusterStateListenerRecord clusterStateListenerRecord = 184 new ClusterStateListenerRecord(executor, callback); 185 if (!mStateListeners.addIfAbsent(clusterStateListenerRecord)) { 186 return; 187 } 188 if (mStateListeners.size() == 1) { 189 try { 190 mService.registerClusterStateListener(mClusterStateListenerBinderCallback); 191 } catch (RemoteException e) { 192 handleRemoteExceptionFromCarService(e); 193 } 194 } 195 } 196 197 /** 198 * Registers the callback for ClusterHome. 199 */ 200 @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE) 201 @AddedInOrBefore(majorVersion = 33) registerClusterNavigationStateListener( @onNull Executor executor, @NonNull ClusterNavigationStateListener callback)202 public void registerClusterNavigationStateListener( 203 @NonNull Executor executor, @NonNull ClusterNavigationStateListener callback) { 204 Objects.requireNonNull(executor, "executor cannot be null"); 205 Objects.requireNonNull(callback, "callback cannot be null"); 206 ClusterNavigationStateListenerRecord clusterStateListenerRecord = 207 new ClusterNavigationStateListenerRecord(executor, callback); 208 if (!mNavigationStateListeners.addIfAbsent(clusterStateListenerRecord)) { 209 return; 210 } 211 if (mNavigationStateListeners.size() == 1) { 212 try { 213 mService.registerClusterNavigationStateListener( 214 mClusterNavigationStateListenerBinderCallback); 215 } catch (RemoteException e) { 216 handleRemoteExceptionFromCarService(e); 217 } 218 } 219 } 220 221 /** 222 * Unregisters the callback. 223 */ 224 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 225 @AddedInOrBefore(majorVersion = 33) unregisterClusterStateListener(@onNull ClusterStateListener callback)226 public void unregisterClusterStateListener(@NonNull ClusterStateListener callback) { 227 Objects.requireNonNull(callback, "callback cannot be null"); 228 if (!mStateListeners 229 .remove(new ClusterStateListenerRecord(/* executor= */ null, callback))) { 230 return; 231 } 232 if (mStateListeners.isEmpty()) { 233 try { 234 mService.unregisterClusterStateListener(mClusterStateListenerBinderCallback); 235 } catch (RemoteException ignored) { 236 // ignore for unregistering 237 } 238 } 239 } 240 241 /** 242 * Unregisters the callback. 243 */ 244 @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE) 245 @AddedInOrBefore(majorVersion = 33) unregisterClusterNavigationStateListener( @onNull ClusterNavigationStateListener callback)246 public void unregisterClusterNavigationStateListener( 247 @NonNull ClusterNavigationStateListener callback) { 248 Objects.requireNonNull(callback, "callback cannot be null"); 249 if (!mNavigationStateListeners.remove(new ClusterNavigationStateListenerRecord( 250 /* executor= */ null, callback))) { 251 return; 252 } 253 if (mNavigationStateListeners.isEmpty()) { 254 try { 255 mService.unregisterClusterNavigationStateListener( 256 mClusterNavigationStateListenerBinderCallback); 257 } catch (RemoteException ignored) { 258 // ignore for unregistering 259 } 260 } 261 } 262 263 private static class IClusterStateListenerImpl extends IClusterStateListener.Stub { 264 private final WeakReference<ClusterHomeManager> mManager; 265 IClusterStateListenerImpl(ClusterHomeManager manager)266 private IClusterStateListenerImpl(ClusterHomeManager manager) { 267 mManager = new WeakReference<>(manager); 268 } 269 270 @Override onClusterStateChanged(@onNull ClusterState state, @Config int changes)271 public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) { 272 ClusterHomeManager manager = mManager.get(); 273 if (manager != null) { 274 for (ClusterStateListenerRecord cb : manager.mStateListeners) { 275 cb.mExecutor.execute( 276 () -> cb.mListener.onClusterStateChanged(state, changes)); 277 } 278 } 279 } 280 } 281 282 private static class IClusterNavigationStateListenerImpl extends 283 IClusterNavigationStateListener.Stub { 284 private final WeakReference<ClusterHomeManager> mManager; 285 IClusterNavigationStateListenerImpl(ClusterHomeManager manager)286 private IClusterNavigationStateListenerImpl(ClusterHomeManager manager) { 287 mManager = new WeakReference<>(manager); 288 } 289 290 @Override onNavigationStateChanged(@onNull byte[] navigationState)291 public void onNavigationStateChanged(@NonNull byte[] navigationState) { 292 ClusterHomeManager manager = mManager.get(); 293 if (manager != null) { 294 for (ClusterNavigationStateListenerRecord lr : manager.mNavigationStateListeners) { 295 lr.mExecutor.execute(() -> lr.mListener.onNavigationState(navigationState)); 296 } 297 } 298 } 299 } 300 301 /** 302 * Reports the current ClusterUI state. 303 * @param uiTypeMain uiType that ClusterHome tries to show in main area 304 * @param uiTypeSub uiType that ClusterHome tries to show in sub area 305 * @param uiAvailability the byte array to represent the availability of ClusterUI. 306 * 0 indicates non-available and 1 indicates available. 307 * Index 0 is reserved for ClusterHome, The other indexes are followed by OEM's definition. 308 */ 309 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 310 @AddedInOrBefore(majorVersion = 33) reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability)311 public void reportState(int uiTypeMain, int uiTypeSub, @NonNull byte[] uiAvailability) { 312 try { 313 mService.reportState(uiTypeMain, uiTypeSub, uiAvailability); 314 } catch (RemoteException e) { 315 handleRemoteExceptionFromCarService(e); 316 } 317 } 318 319 /** 320 * Requests to turn the cluster display on to show some ClusterUI. 321 * @param uiType uiType that ClusterHome tries to show in main area 322 */ 323 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 324 @AddedInOrBefore(majorVersion = 33) requestDisplay(int uiType)325 public void requestDisplay(int uiType) { 326 try { 327 mService.requestDisplay(uiType); 328 } catch (RemoteException e) { 329 handleRemoteExceptionFromCarService(e); 330 } 331 } 332 333 /** 334 * Returns the current {@code ClusterState}. 335 */ 336 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 337 @Nullable 338 @AddedInOrBefore(majorVersion = 33) getClusterState()339 public ClusterState getClusterState() { 340 ClusterState state = null; 341 try { 342 state = mService.getClusterState(); 343 } catch (RemoteException e) { 344 handleRemoteExceptionFromCarService(e); 345 } 346 return state; 347 } 348 349 /** 350 * Start an activity as specified user. The activity is considered as in fixed mode for 351 * the cluster display and will be re-launched if the activity crashes, the package 352 * is updated or goes to background for whatever reason. 353 * Only one activity can exist in fixed mode for the display and calling this multiple 354 * times with different {@code Intent} will lead into making all previous activities into 355 * non-fixed normal state (= will not be re-launched.) 356 * @param intent the Intent to start 357 * @param options additional options for how the Activity should be started 358 * @param userId the user the new activity should run as 359 * @return true if it launches the given Intent as FixedActivity successfully 360 */ 361 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 362 @AddedInOrBefore(majorVersion = 33) startFixedActivityModeAsUser( Intent intent, @Nullable Bundle options, int userId)363 public boolean startFixedActivityModeAsUser( 364 Intent intent, @Nullable Bundle options, int userId) { 365 try { 366 return mService.startFixedActivityModeAsUser(intent, options, userId); 367 } catch (RemoteException e) { 368 handleRemoteExceptionFromCarService(e); 369 } 370 return false; 371 } 372 373 /** 374 * The activity launched on the cluster display is no longer in fixed mode. Re-launching or 375 * finishing should not trigger re-launching any more. Note that Activity for non-current user 376 * will be auto-stopped and there is no need to call this for user switching. Note that this 377 * does not stop the activity but it will not be re-launched any more. 378 */ 379 @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL) 380 @AddedInOrBefore(majorVersion = 33) stopFixedActivityMode()381 public void stopFixedActivityMode() { 382 try { 383 mService.stopFixedActivityMode(); 384 } catch (RemoteException e) { 385 handleRemoteExceptionFromCarService(e); 386 } 387 } 388 389 @Override 390 @AddedInOrBefore(majorVersion = 33) onCarDisconnected()391 protected void onCarDisconnected() { 392 mStateListeners.clear(); 393 mNavigationStateListeners.clear(); 394 } 395 } 396