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