1 /* 2 * Copyright (C) 2015 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; 18 19 import static android.car.feature.Flags.FLAG_PROJECTION_QUERY_BT_PROFILE_INHIBIT; 20 21 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEPRECATED_CODE; 22 23 import android.annotation.CallbackExecutor; 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.bluetooth.BluetoothDevice; 31 import android.car.projection.ProjectionOptions; 32 import android.car.projection.ProjectionStatus; 33 import android.car.projection.ProjectionStatus.ProjectionState; 34 import android.content.Intent; 35 import android.net.wifi.SoftApConfiguration; 36 import android.net.wifi.WifiConfiguration; 37 import android.os.Binder; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.os.IBinder; 41 import android.os.Looper; 42 import android.os.Message; 43 import android.os.Messenger; 44 import android.os.RemoteException; 45 import android.util.ArraySet; 46 import android.util.Pair; 47 import android.util.Slog; 48 import android.view.KeyEvent; 49 50 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 51 import com.android.internal.annotations.GuardedBy; 52 import com.android.internal.util.Preconditions; 53 54 import java.lang.annotation.ElementType; 55 import java.lang.annotation.Retention; 56 import java.lang.annotation.RetentionPolicy; 57 import java.lang.annotation.Target; 58 import java.lang.ref.WeakReference; 59 import java.util.ArrayList; 60 import java.util.BitSet; 61 import java.util.Collections; 62 import java.util.HashMap; 63 import java.util.LinkedHashSet; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Objects; 67 import java.util.Set; 68 import java.util.concurrent.Executor; 69 70 /** 71 * CarProjectionManager allows applications implementing projection to register/unregister itself 72 * with projection manager, listen for voice notification. 73 * 74 * A client must have {@link Car#PERMISSION_CAR_PROJECTION} permission in order to access this 75 * manager. 76 * 77 * @hide 78 */ 79 @SystemApi 80 public final class CarProjectionManager extends CarManagerBase { 81 private static final String TAG = CarProjectionManager.class.getSimpleName(); 82 83 private final Binder mToken = new Binder(); 84 private final Object mLock = new Object(); 85 86 /** 87 * Listener to get projected notifications. 88 * 89 * Currently only voice search request is supported. 90 */ 91 public interface CarProjectionListener { 92 /** 93 * Voice search was requested by the user. 94 */ onVoiceAssistantRequest(boolean fromLongPress)95 void onVoiceAssistantRequest(boolean fromLongPress); 96 } 97 98 /** 99 * Interface for projection apps to receive and handle key events from the system. 100 */ 101 public interface ProjectionKeyEventHandler { 102 /** 103 * Called when a projection key event occurs. 104 * 105 * @param event The projection key event that occurred. 106 */ onKeyEvent(@eyEventNum int event)107 void onKeyEvent(@KeyEventNum int event); 108 } 109 /** 110 * Flag for {@link #registerProjectionListener(CarProjectionListener, int)}: subscribe to 111 * voice-search short-press requests. 112 * 113 * @deprecated Use {@link #addKeyEventHandler(Set, ProjectionKeyEventHandler)} with the 114 * {@link #KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP} event instead. 115 */ 116 @Deprecated 117 public static final int PROJECTION_VOICE_SEARCH = 0x1; 118 /** 119 * Flag for {@link #registerProjectionListener(CarProjectionListener, int)}: subscribe to 120 * voice-search long-press requests. 121 * 122 * @deprecated Use {@link #addKeyEventHandler(Set, ProjectionKeyEventHandler)} with the 123 * {@link #KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN} event instead. 124 */ 125 @Deprecated 126 public static final int PROJECTION_LONG_PRESS_VOICE_SEARCH = 0x2; 127 128 /** 129 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_VOICE_ASSIST} 130 * key is pressed down. 131 * 132 * If the key is released before the long-press timeout, 133 * {@link #KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP} will be fired. If the key is held past the 134 * long-press timeout, {@link #KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN} will be fired, 135 * followed by {@link #KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP}. 136 */ 137 public static final int KEY_EVENT_VOICE_SEARCH_KEY_DOWN = 0; 138 /** 139 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_VOICE_ASSIST} 140 * key is released after a short-press. 141 */ 142 public static final int KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP = 1; 143 /** 144 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_VOICE_ASSIST} 145 * key is held down past the long-press timeout. 146 */ 147 public static final int KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN = 2; 148 /** 149 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_VOICE_ASSIST} 150 * key is released after a long-press. 151 */ 152 public static final int KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP = 3; 153 /** 154 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_CALL} key is 155 * pressed down. 156 * 157 * If the key is released before the long-press timeout, 158 * {@link #KEY_EVENT_CALL_SHORT_PRESS_KEY_UP} will be fired. If the key is held past the 159 * long-press timeout, {@link #KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN} will be fired, followed by 160 * {@link #KEY_EVENT_CALL_LONG_PRESS_KEY_UP}. 161 */ 162 public static final int KEY_EVENT_CALL_KEY_DOWN = 4; 163 /** 164 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_CALL} key is 165 * released after a short-press. 166 */ 167 public static final int KEY_EVENT_CALL_SHORT_PRESS_KEY_UP = 5; 168 /** 169 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_CALL} key is 170 * held down past the long-press timeout. 171 */ 172 public static final int KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN = 6; 173 /** 174 * Event for {@link #addKeyEventHandler}: fired when the {@link KeyEvent#KEYCODE_CALL} key is 175 * released after a long-press. 176 */ 177 public static final int KEY_EVENT_CALL_LONG_PRESS_KEY_UP = 7; 178 179 /** @hide */ 180 public static final int NUM_KEY_EVENTS = 8; 181 182 /** @hide */ 183 @Retention(RetentionPolicy.SOURCE) 184 @IntDef(prefix = "KEY_EVENT_", value = { 185 KEY_EVENT_VOICE_SEARCH_KEY_DOWN, 186 KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP, 187 KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN, 188 KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP, 189 KEY_EVENT_CALL_KEY_DOWN, 190 KEY_EVENT_CALL_SHORT_PRESS_KEY_UP, 191 KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN, 192 KEY_EVENT_CALL_LONG_PRESS_KEY_UP, 193 }) 194 @Target({ElementType.TYPE_USE}) 195 public @interface KeyEventNum {} 196 197 /** @hide */ 198 public static final int PROJECTION_AP_STARTED = 0; 199 /** @hide */ 200 public static final int PROJECTION_AP_STOPPED = 1; 201 /** @hide */ 202 public static final int PROJECTION_AP_FAILED = 2; 203 204 private final ICarProjection mService; 205 private final Executor mHandlerExecutor; 206 207 @GuardedBy("mLock") 208 private CarProjectionListener mListener; 209 @GuardedBy("mLock") 210 private int mVoiceSearchFilter; 211 private final ProjectionKeyEventHandler mLegacyListenerTranslator = 212 this::translateKeyEventToLegacyListener; 213 214 private final ICarProjectionKeyEventHandlerImpl mBinderHandler = 215 new ICarProjectionKeyEventHandlerImpl(this); 216 217 @GuardedBy("mLock") 218 private final Map<ProjectionKeyEventHandler, KeyEventHandlerRecord> mKeyEventHandlers = 219 new HashMap<>(); 220 @GuardedBy("mLock") 221 private BitSet mHandledEvents = new BitSet(); 222 223 private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy; 224 225 private final Set<ProjectionStatusListener> mProjectionStatusListeners = new LinkedHashSet<>(); 226 private CarProjectionStatusListenerImpl mCarProjectionStatusListener; 227 228 // Only one access point proxy object per process. 229 private static final IBinder mAccessPointProxyToken = new Binder(); 230 231 /** 232 * Interface to receive for projection status updates. 233 */ 234 public interface ProjectionStatusListener { 235 /** 236 * This method gets invoked if projection status has been changed. 237 * 238 * @param state - current projection state 239 * @param packageName - if projection is currently running either in the foreground or 240 * in the background this argument will contain its package name 241 * @param details - contains detailed information about all currently registered projection 242 * receivers. 243 */ onProjectionStatusChanged(@rojectionState int state, @Nullable String packageName, @NonNull List<ProjectionStatus> details)244 void onProjectionStatusChanged(@ProjectionState int state, @Nullable String packageName, 245 @NonNull List<ProjectionStatus> details); 246 } 247 248 /** 249 * @hide 250 */ CarProjectionManager(Car car, IBinder service)251 public CarProjectionManager(Car car, IBinder service) { 252 super(car); 253 mService = ICarProjection.Stub.asInterface(service); 254 Handler handler = getEventHandler(); 255 mHandlerExecutor = handler::post; 256 } 257 258 /** 259 * Compatibility with previous APIs due to typo 260 * 261 * @deprecated Use 262 * {@link CarProjectionManager#registerProjectionListener(CarProjectionListener, int)} instead. 263 * @hide 264 */ 265 @ExcludeFromCodeCoverageGeneratedReport(reason = DEPRECATED_CODE) 266 @Deprecated regsiterProjectionListener(CarProjectionListener listener, int voiceSearchFilter)267 public void regsiterProjectionListener(CarProjectionListener listener, int voiceSearchFilter) { 268 registerProjectionListener(listener, voiceSearchFilter); 269 } 270 271 /** 272 * Register listener to monitor projection. Only one listener can be registered and 273 * registering multiple times will lead into only the last listener to be active. 274 * 275 * @param listener the listener to get projected notifications 276 * @param voiceSearchFilter Flags of voice search requests to get notification. 277 */ 278 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) registerProjectionListener(@onNull CarProjectionListener listener, int voiceSearchFilter)279 public void registerProjectionListener(@NonNull CarProjectionListener listener, 280 int voiceSearchFilter) { 281 Objects.requireNonNull(listener, "listener cannot be null"); 282 synchronized (mLock) { 283 if (mListener == null || mVoiceSearchFilter != voiceSearchFilter) { 284 addKeyEventHandler( 285 translateVoiceSearchFilter(voiceSearchFilter), 286 mLegacyListenerTranslator); 287 } 288 mListener = listener; 289 mVoiceSearchFilter = voiceSearchFilter; 290 } 291 } 292 293 /** 294 * Compatibility with previous APIs due to typo 295 * 296 * @deprecated Use {@link CarProjectionManager#unregisterProjectionListener() instead. 297 * @hide 298 */ 299 @ExcludeFromCodeCoverageGeneratedReport(reason = DEPRECATED_CODE) 300 @Deprecated unregsiterProjectionListener()301 public void unregsiterProjectionListener() { 302 unregisterProjectionListener(); 303 } 304 305 /** 306 * Unregister listener and stop listening projection events. 307 */ 308 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) unregisterProjectionListener()309 public void unregisterProjectionListener() { 310 synchronized (mLock) { 311 removeKeyEventHandler(mLegacyListenerTranslator); 312 mListener = null; 313 mVoiceSearchFilter = 0; 314 } 315 } 316 317 @SuppressWarnings("deprecation") translateVoiceSearchFilter(int voiceSearchFilter)318 private static Set<Integer> translateVoiceSearchFilter(int voiceSearchFilter) { 319 Set<Integer> rv = new ArraySet<>(Integer.bitCount(voiceSearchFilter)); 320 if ((voiceSearchFilter & PROJECTION_VOICE_SEARCH) != 0) { 321 rv.add(KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP); 322 } 323 if ((voiceSearchFilter & PROJECTION_LONG_PRESS_VOICE_SEARCH) != 0) { 324 rv.add(KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN); 325 } 326 return rv; 327 } 328 translateKeyEventToLegacyListener(@eyEventNum int keyEvent)329 private void translateKeyEventToLegacyListener(@KeyEventNum int keyEvent) { 330 CarProjectionListener legacyListener; 331 boolean fromLongPress; 332 333 synchronized (mLock) { 334 if (mListener == null) { 335 return; 336 } 337 legacyListener = mListener; 338 339 if (keyEvent == KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP) { 340 fromLongPress = false; 341 } else if (keyEvent == KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN) { 342 fromLongPress = true; 343 } else { 344 Slog.e(TAG, "Unexpected key event " + keyEvent); 345 return; 346 } 347 } 348 349 Slog.d(TAG, "Voice assistant request, long-press = " + fromLongPress); 350 351 legacyListener.onVoiceAssistantRequest(fromLongPress); 352 } 353 354 /** 355 * Adds a {@link ProjectionKeyEventHandler} to be called for the given set of key events. 356 * 357 * If the given event handler is already registered, the event set and {@link Executor} for that 358 * event handler will be replaced with those provided. 359 * 360 * For any event with a defined event handler, the system will suppress its default behavior for 361 * that event, and call the event handler instead. (For instance, if an event handler is defined 362 * for {@link #KEY_EVENT_CALL_SHORT_PRESS_KEY_UP}, the system will not open the dialer when the 363 * {@link KeyEvent#KEYCODE_CALL CALL} key is short-pressed.) 364 * 365 * Callbacks on the event handler will be run on the {@link Handler} designated to run callbacks 366 * from {@link Car}. 367 * 368 * @param events The set of key events to which to subscribe. 369 * @param eventHandler The {@link ProjectionKeyEventHandler} to call when those events occur. 370 */ 371 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) addKeyEventHandler( @onNull Set<@KeyEventNum Integer> events, @NonNull ProjectionKeyEventHandler eventHandler)372 public void addKeyEventHandler( 373 @NonNull Set<@KeyEventNum Integer> events, 374 @NonNull ProjectionKeyEventHandler eventHandler) { 375 addKeyEventHandler(events, null, eventHandler); 376 } 377 378 /** 379 * Adds a {@link ProjectionKeyEventHandler} to be called for the given set of key events. 380 * 381 * If the given event handler is already registered, the event set and {@link Executor} for that 382 * event handler will be replaced with those provided. 383 * 384 * For any event with a defined event handler, the system will suppress its default behavior for 385 * that event, and call the event handler instead. (For instance, if an event handler is defined 386 * for {@link #KEY_EVENT_CALL_SHORT_PRESS_KEY_UP}, the system will not open the dialer when the 387 * {@link KeyEvent#KEYCODE_CALL CALL} key is short-pressed.) 388 * 389 * Callbacks on the event handler will be run on the given {@link Executor}, or, if it is null, 390 * the {@link Handler} designated to run callbacks for {@link Car}. 391 * 392 * @param events The set of key events to which to subscribe. 393 * @param callbackExecutor An {@link Executor} on which to run callbacks. 394 * @param eventHandler The {@link ProjectionKeyEventHandler} to call when those events 395 * occur. 396 */ 397 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) addKeyEventHandler( @onNull Set<@KeyEventNum Integer> events, @CallbackExecutor @Nullable Executor callbackExecutor, @NonNull ProjectionKeyEventHandler eventHandler)398 public void addKeyEventHandler( 399 @NonNull Set<@KeyEventNum Integer> events, 400 @CallbackExecutor @Nullable Executor callbackExecutor, 401 @NonNull ProjectionKeyEventHandler eventHandler) { 402 Executor executor = callbackExecutor; 403 404 BitSet eventMask = new BitSet(); 405 for (int event : events) { 406 Preconditions.checkArgument(event >= 0 && event < NUM_KEY_EVENTS, "Invalid key event"); 407 eventMask.set(event); 408 } 409 410 if (eventMask.isEmpty()) { 411 removeKeyEventHandler(eventHandler); 412 return; 413 } 414 415 if (executor == null) { 416 executor = mHandlerExecutor; 417 } 418 419 synchronized (mLock) { 420 KeyEventHandlerRecord record = mKeyEventHandlers.get(eventHandler); 421 if (record == null) { 422 record = new KeyEventHandlerRecord(executor, eventMask); 423 mKeyEventHandlers.put(eventHandler, record); 424 } else { 425 record.mExecutor = executor; 426 record.mSubscribedEvents = eventMask; 427 } 428 429 updateHandledEventsLocked(); 430 } 431 } 432 433 /** 434 * Removes a previously registered {@link ProjectionKeyEventHandler}. 435 * 436 * @param eventHandler The listener to remove. 437 */ 438 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) removeKeyEventHandler(@onNull ProjectionKeyEventHandler eventHandler)439 public void removeKeyEventHandler(@NonNull ProjectionKeyEventHandler eventHandler) { 440 synchronized (mLock) { 441 KeyEventHandlerRecord record = mKeyEventHandlers.remove(eventHandler); 442 if (record != null) { 443 updateHandledEventsLocked(); 444 } 445 } 446 } 447 448 @GuardedBy("mLock") updateHandledEventsLocked()449 private void updateHandledEventsLocked() { 450 BitSet events = new BitSet(); 451 452 for (KeyEventHandlerRecord record : mKeyEventHandlers.values()) { 453 events.or(record.mSubscribedEvents); 454 } 455 456 if (events.equals(mHandledEvents)) { 457 // No changes. 458 return; 459 } 460 461 try { 462 if (!events.isEmpty()) { 463 Slog.d(TAG, "Registering handler with system for " + events); 464 byte[] eventMask = events.toByteArray(); 465 mService.registerKeyEventHandler(mBinderHandler, eventMask); 466 } else { 467 Slog.d(TAG, "Unregistering handler with system"); 468 mService.unregisterKeyEventHandler(mBinderHandler); 469 } 470 } catch (RemoteException e) { 471 handleRemoteExceptionFromCarService(e); 472 return; 473 } 474 475 mHandledEvents = events; 476 } 477 478 /** 479 * Registers projection runner on projection start with projection service 480 * to create reverse binding. 481 * 482 * @param serviceIntent 483 */ 484 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) registerProjectionRunner(@onNull Intent serviceIntent)485 public void registerProjectionRunner(@NonNull Intent serviceIntent) { 486 Objects.requireNonNull(serviceIntent, "serviceIntent cannot be null"); 487 synchronized (mLock) { 488 try { 489 mService.registerProjectionRunner(serviceIntent); 490 } catch (RemoteException e) { 491 handleRemoteExceptionFromCarService(e); 492 } 493 } 494 } 495 496 /** 497 * Unregisters projection runner on projection stop with projection service to create 498 * reverse binding. 499 * 500 * @param serviceIntent 501 */ 502 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) unregisterProjectionRunner(@onNull Intent serviceIntent)503 public void unregisterProjectionRunner(@NonNull Intent serviceIntent) { 504 Objects.requireNonNull(serviceIntent, "serviceIntent cannot be null"); 505 synchronized (mLock) { 506 try { 507 mService.unregisterProjectionRunner(serviceIntent); 508 } catch (RemoteException e) { 509 handleRemoteExceptionFromCarService(e); 510 } 511 } 512 } 513 514 /** @hide */ 515 @Override onCarDisconnected()516 public void onCarDisconnected() { 517 // nothing to do 518 } 519 520 /** 521 * Request to start Wi-Fi access point if it hasn't been started yet for wireless projection 522 * receiver app. 523 * 524 * <p>A process can have only one request to start an access point, subsequent call of this 525 * method will invalidate previous calls. 526 * 527 * @param callback to receive notifications when access point status changed for the request 528 */ 529 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) startProjectionAccessPoint(@onNull ProjectionAccessPointCallback callback)530 public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) { 531 Objects.requireNonNull(callback, "callback cannot be null"); 532 synchronized (mLock) { 533 Looper looper = getEventHandler().getLooper(); 534 ProjectionAccessPointCallbackProxy proxy = 535 new ProjectionAccessPointCallbackProxy(this, looper, callback); 536 try { 537 mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken); 538 mProjectionAccessPointCallbackProxy = proxy; 539 } catch (RemoteException e) { 540 handleRemoteExceptionFromCarService(e); 541 } 542 } 543 } 544 545 /** 546 * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz, 547 * e.g. channel 1 will be represented as 2412 in the list. 548 * 549 * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*} 550 */ 551 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) getAvailableWifiChannels(int band)552 public @NonNull List<Integer> getAvailableWifiChannels(int band) { 553 try { 554 int[] channels = mService.getAvailableWifiChannels(band); 555 List<Integer> channelList = new ArrayList<>(channels.length); 556 for (int v : channels) { 557 channelList.add(v); 558 } 559 return channelList; 560 } catch (RemoteException e) { 561 return handleRemoteExceptionFromCarService(e, Collections.emptyList()); 562 } 563 } 564 565 /** 566 * Stop Wi-Fi Access Point for wireless projection receiver app. 567 */ 568 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) stopProjectionAccessPoint()569 public void stopProjectionAccessPoint() { 570 ProjectionAccessPointCallbackProxy proxy; 571 synchronized (mLock) { 572 proxy = mProjectionAccessPointCallbackProxy; 573 mProjectionAccessPointCallbackProxy = null; 574 } 575 if (proxy == null) { 576 return; 577 } 578 579 try { 580 mService.stopProjectionAccessPoint(mAccessPointProxyToken); 581 } catch (RemoteException e) { 582 handleRemoteExceptionFromCarService(e); 583 } 584 } 585 586 /** 587 * Request to disconnect the given profile on the given device, and prevent it from reconnecting 588 * until either the request is released, or the process owning the given token dies. Mainly 589 * intended to use with the {@code A2DP_SINK} profile. 590 * 591 * @param device The device on which to inhibit a profile. 592 * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit. 593 * @return True if the profile was successfully inhibited, false if an error occurred. 594 */ 595 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) requestBluetoothProfileInhibit( @onNull BluetoothDevice device, int profile)596 public boolean requestBluetoothProfileInhibit( 597 @NonNull BluetoothDevice device, int profile) { 598 Objects.requireNonNull(device, "device cannot be null"); 599 try { 600 return mService.requestBluetoothProfileInhibit(device, profile, mToken); 601 } catch (RemoteException e) { 602 return handleRemoteExceptionFromCarService(e, false); 603 } 604 } 605 606 /** 607 * Release an inhibit request made by {@link #requestBluetoothProfileInhibit}, and reconnect the 608 * profile if no other inhibit requests are active. Mainly intended to use with the {@code 609 * A2DP_SINK} profile. 610 * 611 * @param device The device on which to release the inhibit request. 612 * @param profile The profile on which to release the inhibit request. 613 * @return True if the request was released, false if an error occurred. 614 */ 615 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) releaseBluetoothProfileInhibit(@onNull BluetoothDevice device, int profile)616 public boolean releaseBluetoothProfileInhibit(@NonNull BluetoothDevice device, int profile) { 617 Objects.requireNonNull(device, "device cannot be null"); 618 try { 619 return mService.releaseBluetoothProfileInhibit(device, profile, mToken); 620 } catch (RemoteException e) { 621 return handleRemoteExceptionFromCarService(e, false); 622 } 623 } 624 625 626 /** 627 * Checks whether a request to disconnect the given profile on the given device has been made 628 * and if the inhibit request is still active. Mainly intended to use with the {@code A2DP_SINK} 629 * profile. 630 * 631 * @param device The device on which to check the inhibit request. 632 * @param profile The profile on which to check the inhibit request. 633 * @return True if inhibit was requested and is still active, false if an error occurred or 634 * inactive. 635 */ 636 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 637 @FlaggedApi(FLAG_PROJECTION_QUERY_BT_PROFILE_INHIBIT) isBluetoothProfileInhibited(@onNull BluetoothDevice device, int profile)638 public boolean isBluetoothProfileInhibited(@NonNull BluetoothDevice device, int profile) { 639 Objects.requireNonNull(device, "device cannot be null"); 640 try { 641 return mService.isBluetoothProfileInhibited(device, profile, mToken); 642 } catch (RemoteException e) { 643 return handleRemoteExceptionFromCarService(e, false); 644 } 645 } 646 647 /** 648 * Call this method to report projection status of your app. The aggregated status (from other 649 * projection apps if available) will be broadcasted to interested parties. 650 * 651 * @param status the reported status that will be distributed to the interested listeners 652 * 653 * @see #registerProjectionStatusListener(ProjectionStatusListener) 654 */ 655 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) updateProjectionStatus(@onNull ProjectionStatus status)656 public void updateProjectionStatus(@NonNull ProjectionStatus status) { 657 Objects.requireNonNull(status, "status cannot be null"); 658 try { 659 mService.updateProjectionStatus(status, mToken); 660 } catch (RemoteException e) { 661 handleRemoteExceptionFromCarService(e); 662 } 663 } 664 665 /** 666 * Register projection status listener. See {@link ProjectionStatusListener} for details. It is 667 * allowed to register multiple listeners. 668 * 669 * <p>Note: provided listener will be called immediately with the most recent status. 670 * 671 * @param listener the listener to receive notification for any projection status changes 672 */ 673 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS) registerProjectionStatusListener(@onNull ProjectionStatusListener listener)674 public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) { 675 Objects.requireNonNull(listener, "listener cannot be null"); 676 synchronized (mLock) { 677 mProjectionStatusListeners.add(listener); 678 679 if (mCarProjectionStatusListener == null) { 680 mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this); 681 try { 682 mService.registerProjectionStatusListener(mCarProjectionStatusListener); 683 } catch (RemoteException e) { 684 handleRemoteExceptionFromCarService(e); 685 } 686 } else { 687 // Already subscribed to Car Service, immediately notify listener with the current 688 // projection status in the event handler thread. 689 int currentProjectionState = mCarProjectionStatusListener.mCurrentState; 690 String currentProjectionPackageName = 691 mCarProjectionStatusListener.mCurrentPackageName; 692 List<ProjectionStatus> projectionStatusDetails = 693 Collections.unmodifiableList(mCarProjectionStatusListener.mDetails); 694 695 getEventHandler().post(() -> 696 listener.onProjectionStatusChanged( 697 currentProjectionState, 698 currentProjectionPackageName, 699 projectionStatusDetails)); 700 } 701 } 702 } 703 704 /** 705 * Unregister provided listener from projection status notifications 706 * 707 * @param listener the listener for projection status notifications that was previously 708 * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)} 709 */ 710 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS) unregisterProjectionStatusListener(@onNull ProjectionStatusListener listener)711 public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) { 712 Objects.requireNonNull(listener, "listener cannot be null"); 713 synchronized (mLock) { 714 if (!mProjectionStatusListeners.remove(listener) 715 || !mProjectionStatusListeners.isEmpty()) { 716 return; 717 } 718 unregisterProjectionStatusListenerFromCarServiceLocked(); 719 } 720 } 721 unregisterProjectionStatusListenerFromCarServiceLocked()722 private void unregisterProjectionStatusListenerFromCarServiceLocked() { 723 try { 724 mService.unregisterProjectionStatusListener(mCarProjectionStatusListener); 725 mCarProjectionStatusListener = null; 726 } catch (RemoteException e) { 727 handleRemoteExceptionFromCarService(e); 728 } 729 } 730 handleProjectionStatusChanged(@rojectionState int state, String packageName, List<ProjectionStatus> details)731 private void handleProjectionStatusChanged(@ProjectionState int state, 732 String packageName, List<ProjectionStatus> details) { 733 List<ProjectionStatusListener> listeners; 734 synchronized (mLock) { 735 listeners = new ArrayList<>(mProjectionStatusListeners); 736 } 737 for (ProjectionStatusListener listener : listeners) { 738 listener.onProjectionStatusChanged(state, packageName, details); 739 } 740 } 741 742 /** 743 * Returns {@link Bundle} object that contains customization for projection app. This bundle 744 * can be parsed using {@link ProjectionOptions}. 745 */ 746 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) getProjectionOptions()747 public @NonNull Bundle getProjectionOptions() { 748 try { 749 return mService.getProjectionOptions(); 750 } catch (RemoteException e) { 751 return handleRemoteExceptionFromCarService(e, Bundle.EMPTY); 752 } 753 } 754 755 /** 756 * Resets projection access point credentials if system was configured to persist local-only 757 * hotspot credentials. 758 */ 759 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) resetProjectionAccessPointCredentials()760 public void resetProjectionAccessPointCredentials() { 761 try { 762 mService.resetProjectionAccessPointCredentials(); 763 } catch (RemoteException e) { 764 handleRemoteExceptionFromCarService(e); 765 } 766 } 767 768 /** 769 * Callback class for applications to receive updates about the LocalOnlyHotspot status. 770 */ 771 public abstract static class ProjectionAccessPointCallback { 772 public static final int ERROR_NO_CHANNEL = 1; 773 public static final int ERROR_GENERIC = 2; 774 public static final int ERROR_INCOMPATIBLE_MODE = 3; 775 public static final int ERROR_TETHERING_DISALLOWED = 4; 776 777 /** 778 * Called when access point started successfully. 779 * <p> 780 * Note that AP detail may contain configuration which is cannot be represented 781 * by the legacy WifiConfiguration, in such cases a null will be returned. 782 * For example: 783 * <li> SoftAp band in {@link WifiConfiguration.apBand} only supports 784 * 2GHz, 5GHz, 2GHz+5GHz bands, so conversion is limited to these bands. </li> 785 * <li> SoftAp security type in {@link WifiConfiguration.KeyMgmt} only supports 786 * NONE, WPA2_PSK, so conversion is limited to these security type.</li> 787 * 788 * @param wifiConfiguration the {@link WifiConfiguration} of the current hotspot. 789 * @deprecated This callback is deprecated. Use {@link #onStarted(SoftApConfiguration))} 790 * instead. 791 */ 792 @Deprecated onStarted(@ullable WifiConfiguration wifiConfiguration)793 public void onStarted(@Nullable WifiConfiguration wifiConfiguration) {} 794 795 /** 796 * Called when access point started successfully. 797 * 798 * @param softApConfiguration the {@link SoftApConfiguration} of the current hotspot. 799 */ onStarted(@onNull SoftApConfiguration softApConfiguration)800 public void onStarted(@NonNull SoftApConfiguration softApConfiguration) { 801 onStarted(softApConfiguration.toWifiConfiguration()); 802 } 803 804 /** Called when access point is stopped. No events will be sent after that. */ onStopped()805 public void onStopped() {} 806 /** Called when access point failed to start. No events will be sent after that. */ onFailed(int reason)807 public void onFailed(int reason) {} 808 } 809 810 /** 811 * Callback proxy for LocalOnlyHotspotCallback objects. 812 */ 813 private static class ProjectionAccessPointCallbackProxy { 814 private static final String LOG_PREFIX = 815 ProjectionAccessPointCallbackProxy.class.getSimpleName() + ": "; 816 817 private final Handler mHandler; 818 private final WeakReference<CarProjectionManager> mCarProjectionManagerRef; 819 private final Messenger mMessenger; 820 ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, final ProjectionAccessPointCallback callback)821 ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, 822 final ProjectionAccessPointCallback callback) { 823 mCarProjectionManagerRef = new WeakReference<>(manager); 824 825 mHandler = new Handler(looper) { 826 @Override 827 public void handleMessage(Message msg) { 828 Slog.d(TAG, LOG_PREFIX + "handle message what: " + msg.what + " msg: " + msg); 829 830 CarProjectionManager manager = mCarProjectionManagerRef.get(); 831 if (manager == null) { 832 Slog.w(TAG, LOG_PREFIX + "handle message post GC"); 833 return; 834 } 835 836 switch (msg.what) { 837 case PROJECTION_AP_STARTED: 838 if (msg.obj == null) { 839 Slog.e(TAG, LOG_PREFIX + "config cannot be null."); 840 callback.onFailed(ProjectionAccessPointCallback.ERROR_GENERIC); 841 return; 842 } 843 if (msg.obj instanceof SoftApConfiguration) { 844 callback.onStarted((SoftApConfiguration) msg.obj); 845 } else if (msg.obj instanceof WifiConfiguration) { 846 callback.onStarted((WifiConfiguration) msg.obj); 847 } 848 break; 849 case PROJECTION_AP_STOPPED: 850 Slog.i(TAG, LOG_PREFIX + "hotspot stopped"); 851 callback.onStopped(); 852 break; 853 case PROJECTION_AP_FAILED: 854 int reasonCode = msg.arg1; 855 Slog.w(TAG, LOG_PREFIX + "failed to start. reason: " 856 + reasonCode); 857 callback.onFailed(reasonCode); 858 break; 859 default: 860 Slog.e(TAG, LOG_PREFIX + "unhandled message. type: " + msg.what); 861 } 862 } 863 }; 864 mMessenger = new Messenger(mHandler); 865 } 866 getMessenger()867 Messenger getMessenger() { 868 return mMessenger; 869 } 870 } 871 872 private static class ICarProjectionKeyEventHandlerImpl 873 extends ICarProjectionKeyEventHandler.Stub { 874 875 private final WeakReference<CarProjectionManager> mManager; 876 ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager)877 private ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager) { 878 mManager = new WeakReference<>(manager); 879 } 880 881 @Override onKeyEvent(@eyEventNum int event)882 public void onKeyEvent(@KeyEventNum int event) { 883 Slog.d(TAG, "Received projection key event " + event); 884 final CarProjectionManager manager = mManager.get(); 885 if (manager == null) { 886 return; 887 } 888 889 List<Pair<ProjectionKeyEventHandler, Executor>> toDispatch = new ArrayList<>(); 890 synchronized (manager.mLock) { 891 for (Map.Entry<ProjectionKeyEventHandler, KeyEventHandlerRecord> entry : 892 manager.mKeyEventHandlers.entrySet()) { 893 if (entry.getValue().mSubscribedEvents.get(event)) { 894 toDispatch.add(Pair.create(entry.getKey(), entry.getValue().mExecutor)); 895 } 896 } 897 } 898 899 for (Pair<ProjectionKeyEventHandler, Executor> entry : toDispatch) { 900 ProjectionKeyEventHandler listener = entry.first; 901 entry.second.execute(() -> listener.onKeyEvent(event)); 902 } 903 } 904 } 905 906 private static class KeyEventHandlerRecord { 907 @NonNull Executor mExecutor; 908 @NonNull BitSet mSubscribedEvents; 909 KeyEventHandlerRecord(@onNull Executor executor, @NonNull BitSet subscribedEvents)910 KeyEventHandlerRecord(@NonNull Executor executor, @NonNull BitSet subscribedEvents) { 911 mExecutor = executor; 912 mSubscribedEvents = subscribedEvents; 913 } 914 } 915 916 private static class CarProjectionStatusListenerImpl 917 extends ICarProjectionStatusListener.Stub { 918 919 private @ProjectionState int mCurrentState; 920 private @Nullable String mCurrentPackageName; 921 private List<ProjectionStatus> mDetails = new ArrayList<>(0); 922 923 private final WeakReference<CarProjectionManager> mManagerRef; 924 CarProjectionStatusListenerImpl(CarProjectionManager mgr)925 private CarProjectionStatusListenerImpl(CarProjectionManager mgr) { 926 mManagerRef = new WeakReference<>(mgr); 927 } 928 929 @Override onProjectionStatusChanged(int projectionState, String packageName, List<ProjectionStatus> details)930 public void onProjectionStatusChanged(int projectionState, 931 String packageName, 932 List<ProjectionStatus> details) { 933 CarProjectionManager mgr = mManagerRef.get(); 934 if (mgr != null) { 935 mgr.getEventHandler().post(() -> { 936 mCurrentState = projectionState; 937 mCurrentPackageName = packageName; 938 mDetails = Collections.unmodifiableList(details); 939 940 mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails); 941 }); 942 } 943 } 944 } 945 } 946