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 extends 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 Executor mHandlerExecutor; 198 199 @GuardedBy("mLock") 200 private CarProjectionListener mListener; 201 @GuardedBy("mLock") 202 private int mVoiceSearchFilter; 203 private final ProjectionKeyEventHandler mLegacyListenerTranslator = 204 this::translateKeyEventToLegacyListener; 205 206 private final ICarProjectionKeyEventHandlerImpl mBinderHandler = 207 new ICarProjectionKeyEventHandlerImpl(this); 208 209 @GuardedBy("mLock") 210 private final Map<ProjectionKeyEventHandler, KeyEventHandlerRecord> mKeyEventHandlers = 211 new HashMap<>(); 212 @GuardedBy("mLock") 213 private BitSet mHandledEvents = new BitSet(); 214 215 private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy; 216 217 private final Set<ProjectionStatusListener> mProjectionStatusListeners = new LinkedHashSet<>(); 218 private CarProjectionStatusListenerImpl mCarProjectionStatusListener; 219 220 // Only one access point proxy object per process. 221 private static final IBinder mAccessPointProxyToken = new Binder(); 222 223 /** 224 * Interface to receive for projection status updates. 225 */ 226 public interface ProjectionStatusListener { 227 /** 228 * This method gets invoked if projection status has been changed. 229 * 230 * @param state - current projection state 231 * @param packageName - if projection is currently running either in the foreground or 232 * in the background this argument will contain its package name 233 * @param details - contains detailed information about all currently registered projection 234 * receivers. 235 */ onProjectionStatusChanged(@rojectionState int state, @Nullable String packageName, @NonNull List<ProjectionStatus> details)236 void onProjectionStatusChanged(@ProjectionState int state, @Nullable String packageName, 237 @NonNull List<ProjectionStatus> details); 238 } 239 240 /** 241 * @hide 242 */ CarProjectionManager(Car car, IBinder service)243 public CarProjectionManager(Car car, IBinder service) { 244 super(car); 245 mService = ICarProjection.Stub.asInterface(service); 246 Handler handler = getEventHandler(); 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 handleRemoteExceptionFromCarService(e); 452 return; 453 } 454 455 mHandledEvents = events; 456 } 457 458 /** 459 * Registers projection runner on projection start with projection service 460 * to create reverse binding. 461 * @param serviceIntent 462 */ 463 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) registerProjectionRunner(@onNull Intent serviceIntent)464 public void registerProjectionRunner(@NonNull Intent serviceIntent) { 465 Preconditions.checkNotNull("serviceIntent cannot be null"); 466 synchronized (mLock) { 467 try { 468 mService.registerProjectionRunner(serviceIntent); 469 } catch (RemoteException e) { 470 handleRemoteExceptionFromCarService(e); 471 } 472 } 473 } 474 475 /** 476 * Unregisters projection runner on projection stop with projection service to create 477 * reverse binding. 478 * @param serviceIntent 479 */ 480 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) unregisterProjectionRunner(@onNull Intent serviceIntent)481 public void unregisterProjectionRunner(@NonNull Intent serviceIntent) { 482 Preconditions.checkNotNull("serviceIntent cannot be null"); 483 synchronized (mLock) { 484 try { 485 mService.unregisterProjectionRunner(serviceIntent); 486 } catch (RemoteException e) { 487 handleRemoteExceptionFromCarService(e); 488 } 489 } 490 } 491 492 /** @hide */ 493 @Override onCarDisconnected()494 public void onCarDisconnected() { 495 // nothing to do 496 } 497 498 /** 499 * Request to start Wi-Fi access point if it hasn't been started yet for wireless projection 500 * receiver app. 501 * 502 * <p>A process can have only one request to start an access point, subsequent call of this 503 * method will invalidate previous calls. 504 * 505 * @param callback to receive notifications when access point status changed for the request 506 */ 507 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) startProjectionAccessPoint(@onNull ProjectionAccessPointCallback callback)508 public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) { 509 Preconditions.checkNotNull(callback, "callback cannot be null"); 510 synchronized (mLock) { 511 Looper looper = getEventHandler().getLooper(); 512 ProjectionAccessPointCallbackProxy proxy = 513 new ProjectionAccessPointCallbackProxy(this, looper, callback); 514 try { 515 mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken); 516 mProjectionAccessPointCallbackProxy = proxy; 517 } catch (RemoteException e) { 518 handleRemoteExceptionFromCarService(e); 519 } 520 } 521 } 522 523 /** 524 * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz, 525 * e.g. channel 1 will be represented as 2412 in the list. 526 * 527 * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*} 528 */ 529 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) getAvailableWifiChannels(int band)530 public @NonNull List<Integer> getAvailableWifiChannels(int band) { 531 try { 532 int[] channels = mService.getAvailableWifiChannels(band); 533 List<Integer> channelList = new ArrayList<>(channels.length); 534 for (int v : channels) { 535 channelList.add(v); 536 } 537 return channelList; 538 } catch (RemoteException e) { 539 return handleRemoteExceptionFromCarService(e, Collections.emptyList()); 540 } 541 } 542 543 /** 544 * Stop Wi-Fi Access Point for wireless projection receiver app. 545 */ 546 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) stopProjectionAccessPoint()547 public void stopProjectionAccessPoint() { 548 ProjectionAccessPointCallbackProxy proxy; 549 synchronized (mLock) { 550 proxy = mProjectionAccessPointCallbackProxy; 551 mProjectionAccessPointCallbackProxy = null; 552 } 553 if (proxy == null) { 554 return; 555 } 556 557 try { 558 mService.stopProjectionAccessPoint(mAccessPointProxyToken); 559 } catch (RemoteException e) { 560 handleRemoteExceptionFromCarService(e); 561 } 562 } 563 564 /** 565 * Request to disconnect the given profile on the given device, and prevent it from reconnecting 566 * until either the request is released, or the process owning the given token dies. 567 * 568 * @param device The device on which to inhibit a profile. 569 * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit. 570 * @return True if the profile was successfully inhibited, false if an error occurred. 571 */ 572 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) requestBluetoothProfileInhibit( @onNull BluetoothDevice device, int profile)573 public boolean requestBluetoothProfileInhibit( 574 @NonNull BluetoothDevice device, int profile) { 575 Preconditions.checkNotNull(device, "device cannot be null"); 576 try { 577 return mService.requestBluetoothProfileInhibit(device, profile, mToken); 578 } catch (RemoteException e) { 579 return handleRemoteExceptionFromCarService(e, false); 580 } 581 } 582 583 /** 584 * Release an inhibit request made by {@link #requestBluetoothProfileInhibit}, and reconnect the 585 * profile if no other inhibit requests are active. 586 * 587 * @param device The device on which to release the inhibit request. 588 * @param profile The profile on which to release the inhibit request. 589 * @return True if the request was released, false if an error occurred. 590 */ 591 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) releaseBluetoothProfileInhibit(@onNull BluetoothDevice device, int profile)592 public boolean releaseBluetoothProfileInhibit(@NonNull BluetoothDevice device, int profile) { 593 Preconditions.checkNotNull(device, "device cannot be null"); 594 try { 595 return mService.releaseBluetoothProfileInhibit(device, profile, mToken); 596 } catch (RemoteException e) { 597 return handleRemoteExceptionFromCarService(e, false); 598 } 599 } 600 601 /** 602 * Call this method to report projection status of your app. The aggregated status (from other 603 * projection apps if available) will be broadcasted to interested parties. 604 * 605 * @param status the reported status that will be distributed to the interested listeners 606 * 607 * @see #registerProjectionStatusListener(ProjectionStatusListener) 608 */ 609 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) updateProjectionStatus(@onNull ProjectionStatus status)610 public void updateProjectionStatus(@NonNull ProjectionStatus status) { 611 Preconditions.checkNotNull(status, "status cannot be null"); 612 try { 613 mService.updateProjectionStatus(status, mToken); 614 } catch (RemoteException e) { 615 handleRemoteExceptionFromCarService(e); 616 } 617 } 618 619 /** 620 * Register projection status listener. See {@link ProjectionStatusListener} for details. It is 621 * allowed to register multiple listeners. 622 * 623 * <p>Note: provided listener will be called immediately with the most recent status. 624 * 625 * @param listener the listener to receive notification for any projection status changes 626 */ 627 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS) registerProjectionStatusListener(@onNull ProjectionStatusListener listener)628 public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) { 629 Preconditions.checkNotNull(listener, "listener cannot be null"); 630 synchronized (mLock) { 631 mProjectionStatusListeners.add(listener); 632 633 if (mCarProjectionStatusListener == null) { 634 mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this); 635 try { 636 mService.registerProjectionStatusListener(mCarProjectionStatusListener); 637 } catch (RemoteException e) { 638 handleRemoteExceptionFromCarService(e); 639 } 640 } else { 641 // Already subscribed to Car Service, immediately notify listener with the current 642 // projection status in the event handler thread. 643 getEventHandler().post(() -> 644 listener.onProjectionStatusChanged( 645 mCarProjectionStatusListener.mCurrentState, 646 mCarProjectionStatusListener.mCurrentPackageName, 647 mCarProjectionStatusListener.mDetails)); 648 } 649 } 650 } 651 652 /** 653 * Unregister provided listener from projection status notifications 654 * 655 * @param listener the listener for projection status notifications that was previously 656 * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)} 657 */ 658 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS) unregisterProjectionStatusListener(@onNull ProjectionStatusListener listener)659 public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) { 660 Preconditions.checkNotNull(listener, "listener cannot be null"); 661 synchronized (mLock) { 662 if (!mProjectionStatusListeners.remove(listener) 663 || !mProjectionStatusListeners.isEmpty()) { 664 return; 665 } 666 unregisterProjectionStatusListenerFromCarServiceLocked(); 667 } 668 } 669 unregisterProjectionStatusListenerFromCarServiceLocked()670 private void unregisterProjectionStatusListenerFromCarServiceLocked() { 671 try { 672 mService.unregisterProjectionStatusListener(mCarProjectionStatusListener); 673 mCarProjectionStatusListener = null; 674 } catch (RemoteException e) { 675 handleRemoteExceptionFromCarService(e); 676 } 677 } 678 handleProjectionStatusChanged(@rojectionState int state, String packageName, List<ProjectionStatus> details)679 private void handleProjectionStatusChanged(@ProjectionState int state, 680 String packageName, List<ProjectionStatus> details) { 681 List<ProjectionStatusListener> listeners; 682 synchronized (mLock) { 683 listeners = new ArrayList<>(mProjectionStatusListeners); 684 } 685 for (ProjectionStatusListener listener : listeners) { 686 listener.onProjectionStatusChanged(state, packageName, details); 687 } 688 } 689 690 /** 691 * Returns {@link Bundle} object that contains customization for projection app. This bundle 692 * can be parsed using {@link ProjectionOptions}. 693 */ 694 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) getProjectionOptions()695 public @NonNull Bundle getProjectionOptions() { 696 try { 697 return mService.getProjectionOptions(); 698 } catch (RemoteException e) { 699 return handleRemoteExceptionFromCarService(e, Bundle.EMPTY); 700 } 701 } 702 703 /** 704 * Callback class for applications to receive updates about the LocalOnlyHotspot status. 705 */ 706 public abstract static class ProjectionAccessPointCallback { 707 public static final int ERROR_NO_CHANNEL = 1; 708 public static final int ERROR_GENERIC = 2; 709 public static final int ERROR_INCOMPATIBLE_MODE = 3; 710 public static final int ERROR_TETHERING_DISALLOWED = 4; 711 712 /** Called when access point started successfully. */ onStarted(WifiConfiguration wifiConfiguration)713 public void onStarted(WifiConfiguration wifiConfiguration) {} 714 /** Called when access point is stopped. No events will be sent after that. */ onStopped()715 public void onStopped() {} 716 /** Called when access point failed to start. No events will be sent after that. */ onFailed(int reason)717 public void onFailed(int reason) {} 718 } 719 720 /** 721 * Callback proxy for LocalOnlyHotspotCallback objects. 722 */ 723 private static class ProjectionAccessPointCallbackProxy { 724 private static final String LOG_PREFIX = 725 ProjectionAccessPointCallbackProxy.class.getSimpleName() + ": "; 726 727 private final Handler mHandler; 728 private final WeakReference<CarProjectionManager> mCarProjectionManagerRef; 729 private final Messenger mMessenger; 730 ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, final ProjectionAccessPointCallback callback)731 ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, 732 final ProjectionAccessPointCallback callback) { 733 mCarProjectionManagerRef = new WeakReference<>(manager); 734 735 mHandler = new Handler(looper) { 736 @Override 737 public void handleMessage(Message msg) { 738 Log.d(TAG, LOG_PREFIX + "handle message what: " + msg.what + " msg: " + msg); 739 740 CarProjectionManager manager = mCarProjectionManagerRef.get(); 741 if (manager == null) { 742 Log.w(TAG, LOG_PREFIX + "handle message post GC"); 743 return; 744 } 745 746 switch (msg.what) { 747 case PROJECTION_AP_STARTED: 748 WifiConfiguration config = (WifiConfiguration) msg.obj; 749 if (config == null) { 750 Log.e(TAG, LOG_PREFIX + "config cannot be null."); 751 callback.onFailed(ProjectionAccessPointCallback.ERROR_GENERIC); 752 return; 753 } 754 callback.onStarted(config); 755 break; 756 case PROJECTION_AP_STOPPED: 757 Log.i(TAG, LOG_PREFIX + "hotspot stopped"); 758 callback.onStopped(); 759 break; 760 case PROJECTION_AP_FAILED: 761 int reasonCode = msg.arg1; 762 Log.w(TAG, LOG_PREFIX + "failed to start. reason: " 763 + reasonCode); 764 callback.onFailed(reasonCode); 765 break; 766 default: 767 Log.e(TAG, LOG_PREFIX + "unhandled message. type: " + msg.what); 768 } 769 } 770 }; 771 mMessenger = new Messenger(mHandler); 772 } 773 getMessenger()774 Messenger getMessenger() { 775 return mMessenger; 776 } 777 } 778 779 private static class ICarProjectionKeyEventHandlerImpl 780 extends ICarProjectionKeyEventHandler.Stub { 781 782 private final WeakReference<CarProjectionManager> mManager; 783 ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager)784 private ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager) { 785 mManager = new WeakReference<>(manager); 786 } 787 788 @Override onKeyEvent(@eyEventNum int event)789 public void onKeyEvent(@KeyEventNum int event) { 790 Log.d(TAG, "Received projection key event " + event); 791 final CarProjectionManager manager = mManager.get(); 792 if (manager == null) { 793 return; 794 } 795 796 List<Pair<ProjectionKeyEventHandler, Executor>> toDispatch = new ArrayList<>(); 797 synchronized (manager.mLock) { 798 for (Map.Entry<ProjectionKeyEventHandler, KeyEventHandlerRecord> entry : 799 manager.mKeyEventHandlers.entrySet()) { 800 if (entry.getValue().mSubscribedEvents.get(event)) { 801 toDispatch.add(Pair.create(entry.getKey(), entry.getValue().mExecutor)); 802 } 803 } 804 } 805 806 for (Pair<ProjectionKeyEventHandler, Executor> entry : toDispatch) { 807 ProjectionKeyEventHandler listener = entry.first; 808 entry.second.execute(() -> listener.onKeyEvent(event)); 809 } 810 } 811 } 812 813 private static class KeyEventHandlerRecord { 814 @NonNull Executor mExecutor; 815 @NonNull BitSet mSubscribedEvents; 816 KeyEventHandlerRecord(@onNull Executor executor, @NonNull BitSet subscribedEvents)817 KeyEventHandlerRecord(@NonNull Executor executor, @NonNull BitSet subscribedEvents) { 818 mExecutor = executor; 819 mSubscribedEvents = subscribedEvents; 820 } 821 } 822 823 private static class CarProjectionStatusListenerImpl 824 extends ICarProjectionStatusListener.Stub { 825 826 private @ProjectionState int mCurrentState; 827 private @Nullable String mCurrentPackageName; 828 private List<ProjectionStatus> mDetails = new ArrayList<>(0); 829 830 private final WeakReference<CarProjectionManager> mManagerRef; 831 CarProjectionStatusListenerImpl(CarProjectionManager mgr)832 private CarProjectionStatusListenerImpl(CarProjectionManager mgr) { 833 mManagerRef = new WeakReference<>(mgr); 834 } 835 836 @Override onProjectionStatusChanged(int projectionState, String packageName, List<ProjectionStatus> details)837 public void onProjectionStatusChanged(int projectionState, 838 String packageName, 839 List<ProjectionStatus> details) { 840 CarProjectionManager mgr = mManagerRef.get(); 841 if (mgr != null) { 842 mgr.getEventHandler().post(() -> { 843 mCurrentState = projectionState; 844 mCurrentPackageName = packageName; 845 mDetails = Collections.unmodifiableList(details); 846 847 mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails); 848 }); 849 } 850 } 851 } 852 } 853