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