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