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 if ((voiceSearchFilter & PROJECTION_VOICE_SEARCH) != 0) { 340 rv.add(KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP); 341 } 342 if ((voiceSearchFilter & PROJECTION_LONG_PRESS_VOICE_SEARCH) != 0) { 343 rv.add(KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN); 344 } 345 return rv; 346 } 347 translateKeyEventToLegacyListener(@eyEventNum int keyEvent)348 private void translateKeyEventToLegacyListener(@KeyEventNum int keyEvent) { 349 CarProjectionListener legacyListener; 350 boolean fromLongPress; 351 352 synchronized (mLock) { 353 if (mListener == null) { 354 return; 355 } 356 legacyListener = mListener; 357 358 if (keyEvent == KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP) { 359 fromLongPress = false; 360 } else if (keyEvent == KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN) { 361 fromLongPress = true; 362 } else { 363 Log.e(TAG, "Unexpected key event " + keyEvent); 364 return; 365 } 366 } 367 368 Log.d(TAG, "Voice assistant request, long-press = " + fromLongPress); 369 370 legacyListener.onVoiceAssistantRequest(fromLongPress); 371 } 372 373 /** 374 * Adds a {@link ProjectionKeyEventHandler} to be called for the given set of key events. 375 * 376 * If the given event handler is already registered, the event set and {@link Executor} for that 377 * event handler will be replaced with those provided. 378 * 379 * For any event with a defined event handler, the system will suppress its default behavior for 380 * that event, and call the event handler instead. (For instance, if an event handler is defined 381 * for {@link #KEY_EVENT_CALL_SHORT_PRESS_KEY_UP}, the system will not open the dialer when the 382 * {@link KeyEvent#KEYCODE_CALL CALL} key is short-pressed.) 383 * 384 * Callbacks on the event handler will be run on the {@link Handler} designated to run callbacks 385 * from {@link Car}. 386 * 387 * @param events The set of key events to which to subscribe. 388 * @param eventHandler The {@link ProjectionKeyEventHandler} to call when those events occur. 389 */ 390 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 391 @AddedInOrBefore(majorVersion = 33) addKeyEventHandler( @onNull Set<@KeyEventNum Integer> events, @NonNull ProjectionKeyEventHandler eventHandler)392 public void addKeyEventHandler( 393 @NonNull Set<@KeyEventNum Integer> events, 394 @NonNull ProjectionKeyEventHandler eventHandler) { 395 addKeyEventHandler(events, null, eventHandler); 396 } 397 398 /** 399 * Adds a {@link ProjectionKeyEventHandler} to be called for the given set of key events. 400 * 401 * If the given event handler is already registered, the event set and {@link Executor} for that 402 * event handler will be replaced with those provided. 403 * 404 * For any event with a defined event handler, the system will suppress its default behavior for 405 * that event, and call the event handler instead. (For instance, if an event handler is defined 406 * for {@link #KEY_EVENT_CALL_SHORT_PRESS_KEY_UP}, the system will not open the dialer when the 407 * {@link KeyEvent#KEYCODE_CALL CALL} key is short-pressed.) 408 * 409 * Callbacks on the event handler will be run on the given {@link Executor}, or, if it is null, 410 * the {@link Handler} designated to run callbacks for {@link Car}. 411 * 412 * @param events The set of key events to which to subscribe. 413 * @param callbackExecutor An {@link Executor} on which to run callbacks. 414 * @param eventHandler The {@link ProjectionKeyEventHandler} to call when those events 415 * occur. 416 */ 417 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 418 @AddedInOrBefore(majorVersion = 33) addKeyEventHandler( @onNull Set<@KeyEventNum Integer> events, @CallbackExecutor @Nullable Executor callbackExecutor, @NonNull ProjectionKeyEventHandler eventHandler)419 public void addKeyEventHandler( 420 @NonNull Set<@KeyEventNum Integer> events, 421 @CallbackExecutor @Nullable Executor callbackExecutor, 422 @NonNull ProjectionKeyEventHandler eventHandler) { 423 Executor executor = callbackExecutor; 424 425 BitSet eventMask = new BitSet(); 426 for (int event : events) { 427 Preconditions.checkArgument(event >= 0 && event < NUM_KEY_EVENTS, "Invalid key event"); 428 eventMask.set(event); 429 } 430 431 if (eventMask.isEmpty()) { 432 removeKeyEventHandler(eventHandler); 433 return; 434 } 435 436 if (executor == null) { 437 executor = mHandlerExecutor; 438 } 439 440 synchronized (mLock) { 441 KeyEventHandlerRecord record = mKeyEventHandlers.get(eventHandler); 442 if (record == null) { 443 record = new KeyEventHandlerRecord(executor, eventMask); 444 mKeyEventHandlers.put(eventHandler, record); 445 } else { 446 record.mExecutor = executor; 447 record.mSubscribedEvents = eventMask; 448 } 449 450 updateHandledEventsLocked(); 451 } 452 } 453 454 /** 455 * Removes a previously registered {@link ProjectionKeyEventHandler}. 456 * 457 * @param eventHandler The listener to remove. 458 */ 459 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 460 @AddedInOrBefore(majorVersion = 33) removeKeyEventHandler(@onNull ProjectionKeyEventHandler eventHandler)461 public void removeKeyEventHandler(@NonNull ProjectionKeyEventHandler eventHandler) { 462 synchronized (mLock) { 463 KeyEventHandlerRecord record = mKeyEventHandlers.remove(eventHandler); 464 if (record != null) { 465 updateHandledEventsLocked(); 466 } 467 } 468 } 469 470 @GuardedBy("mLock") updateHandledEventsLocked()471 private void updateHandledEventsLocked() { 472 BitSet events = new BitSet(); 473 474 for (KeyEventHandlerRecord record : mKeyEventHandlers.values()) { 475 events.or(record.mSubscribedEvents); 476 } 477 478 if (events.equals(mHandledEvents)) { 479 // No changes. 480 return; 481 } 482 483 try { 484 if (!events.isEmpty()) { 485 Log.d(TAG, "Registering handler with system for " + events); 486 byte[] eventMask = events.toByteArray(); 487 mService.registerKeyEventHandler(mBinderHandler, eventMask); 488 } else { 489 Log.d(TAG, "Unregistering handler with system"); 490 mService.unregisterKeyEventHandler(mBinderHandler); 491 } 492 } catch (RemoteException e) { 493 handleRemoteExceptionFromCarService(e); 494 return; 495 } 496 497 mHandledEvents = events; 498 } 499 500 /** 501 * Registers projection runner on projection start with projection service 502 * to create reverse binding. 503 * 504 * @param serviceIntent 505 */ 506 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 507 @AddedInOrBefore(majorVersion = 33) registerProjectionRunner(@onNull Intent serviceIntent)508 public void registerProjectionRunner(@NonNull Intent serviceIntent) { 509 Objects.requireNonNull(serviceIntent, "serviceIntent cannot be null"); 510 synchronized (mLock) { 511 try { 512 mService.registerProjectionRunner(serviceIntent); 513 } catch (RemoteException e) { 514 handleRemoteExceptionFromCarService(e); 515 } 516 } 517 } 518 519 /** 520 * Unregisters projection runner on projection stop with projection service to create 521 * reverse binding. 522 * 523 * @param serviceIntent 524 */ 525 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 526 @AddedInOrBefore(majorVersion = 33) unregisterProjectionRunner(@onNull Intent serviceIntent)527 public void unregisterProjectionRunner(@NonNull Intent serviceIntent) { 528 Objects.requireNonNull(serviceIntent, "serviceIntent cannot be null"); 529 synchronized (mLock) { 530 try { 531 mService.unregisterProjectionRunner(serviceIntent); 532 } catch (RemoteException e) { 533 handleRemoteExceptionFromCarService(e); 534 } 535 } 536 } 537 538 /** @hide */ 539 @Override 540 @AddedInOrBefore(majorVersion = 33) onCarDisconnected()541 public void onCarDisconnected() { 542 // nothing to do 543 } 544 545 /** 546 * Request to start Wi-Fi access point if it hasn't been started yet for wireless projection 547 * receiver app. 548 * 549 * <p>A process can have only one request to start an access point, subsequent call of this 550 * method will invalidate previous calls. 551 * 552 * @param callback to receive notifications when access point status changed for the request 553 */ 554 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 555 @AddedInOrBefore(majorVersion = 33) startProjectionAccessPoint(@onNull ProjectionAccessPointCallback callback)556 public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) { 557 Objects.requireNonNull(callback, "callback cannot be null"); 558 synchronized (mLock) { 559 Looper looper = getEventHandler().getLooper(); 560 ProjectionAccessPointCallbackProxy proxy = 561 new ProjectionAccessPointCallbackProxy(this, looper, callback); 562 try { 563 mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken); 564 mProjectionAccessPointCallbackProxy = proxy; 565 } catch (RemoteException e) { 566 handleRemoteExceptionFromCarService(e); 567 } 568 } 569 } 570 571 /** 572 * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz, 573 * e.g. channel 1 will be represented as 2412 in the list. 574 * 575 * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*} 576 */ 577 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 578 @AddedInOrBefore(majorVersion = 33) getAvailableWifiChannels(int band)579 public @NonNull List<Integer> getAvailableWifiChannels(int band) { 580 try { 581 int[] channels = mService.getAvailableWifiChannels(band); 582 List<Integer> channelList = new ArrayList<>(channels.length); 583 for (int v : channels) { 584 channelList.add(v); 585 } 586 return channelList; 587 } catch (RemoteException e) { 588 return handleRemoteExceptionFromCarService(e, Collections.emptyList()); 589 } 590 } 591 592 /** 593 * Stop Wi-Fi Access Point for wireless projection receiver app. 594 */ 595 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 596 @AddedInOrBefore(majorVersion = 33) stopProjectionAccessPoint()597 public void stopProjectionAccessPoint() { 598 ProjectionAccessPointCallbackProxy proxy; 599 synchronized (mLock) { 600 proxy = mProjectionAccessPointCallbackProxy; 601 mProjectionAccessPointCallbackProxy = null; 602 } 603 if (proxy == null) { 604 return; 605 } 606 607 try { 608 mService.stopProjectionAccessPoint(mAccessPointProxyToken); 609 } catch (RemoteException e) { 610 handleRemoteExceptionFromCarService(e); 611 } 612 } 613 614 /** 615 * Request to disconnect the given profile on the given device, and prevent it from reconnecting 616 * until either the request is released, or the process owning the given token dies. 617 * 618 * @param device The device on which to inhibit a profile. 619 * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit. 620 * @return True if the profile was successfully inhibited, false if an error occurred. 621 */ 622 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 623 @AddedInOrBefore(majorVersion = 33) requestBluetoothProfileInhibit( @onNull BluetoothDevice device, int profile)624 public boolean requestBluetoothProfileInhibit( 625 @NonNull BluetoothDevice device, int profile) { 626 Objects.requireNonNull(device, "device cannot be null"); 627 try { 628 return mService.requestBluetoothProfileInhibit(device, profile, mToken); 629 } catch (RemoteException e) { 630 return handleRemoteExceptionFromCarService(e, false); 631 } 632 } 633 634 /** 635 * Release an inhibit request made by {@link #requestBluetoothProfileInhibit}, and reconnect the 636 * profile if no other inhibit requests are active. 637 * 638 * @param device The device on which to release the inhibit request. 639 * @param profile The profile on which to release the inhibit request. 640 * @return True if the request was released, false if an error occurred. 641 */ 642 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 643 @AddedInOrBefore(majorVersion = 33) releaseBluetoothProfileInhibit(@onNull BluetoothDevice device, int profile)644 public boolean releaseBluetoothProfileInhibit(@NonNull BluetoothDevice device, int profile) { 645 Objects.requireNonNull(device, "device cannot be null"); 646 try { 647 return mService.releaseBluetoothProfileInhibit(device, profile, mToken); 648 } catch (RemoteException e) { 649 return handleRemoteExceptionFromCarService(e, false); 650 } 651 } 652 653 /** 654 * Call this method to report projection status of your app. The aggregated status (from other 655 * projection apps if available) will be broadcasted to interested parties. 656 * 657 * @param status the reported status that will be distributed to the interested listeners 658 * 659 * @see #registerProjectionStatusListener(ProjectionStatusListener) 660 */ 661 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 662 @AddedInOrBefore(majorVersion = 33) updateProjectionStatus(@onNull ProjectionStatus status)663 public void updateProjectionStatus(@NonNull ProjectionStatus status) { 664 Objects.requireNonNull(status, "status cannot be null"); 665 try { 666 mService.updateProjectionStatus(status, mToken); 667 } catch (RemoteException e) { 668 handleRemoteExceptionFromCarService(e); 669 } 670 } 671 672 /** 673 * Register projection status listener. See {@link ProjectionStatusListener} for details. It is 674 * allowed to register multiple listeners. 675 * 676 * <p>Note: provided listener will be called immediately with the most recent status. 677 * 678 * @param listener the listener to receive notification for any projection status changes 679 */ 680 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS) 681 @AddedInOrBefore(majorVersion = 33) registerProjectionStatusListener(@onNull ProjectionStatusListener listener)682 public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) { 683 Objects.requireNonNull(listener, "listener cannot be null"); 684 synchronized (mLock) { 685 mProjectionStatusListeners.add(listener); 686 687 if (mCarProjectionStatusListener == null) { 688 mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this); 689 try { 690 mService.registerProjectionStatusListener(mCarProjectionStatusListener); 691 } catch (RemoteException e) { 692 handleRemoteExceptionFromCarService(e); 693 } 694 } else { 695 // Already subscribed to Car Service, immediately notify listener with the current 696 // projection status in the event handler thread. 697 int currentProjectionState = mCarProjectionStatusListener.mCurrentState; 698 String currentProjectionPackageName = 699 mCarProjectionStatusListener.mCurrentPackageName; 700 List<ProjectionStatus> projectionStatusDetails = 701 Collections.unmodifiableList(mCarProjectionStatusListener.mDetails); 702 703 getEventHandler().post(() -> 704 listener.onProjectionStatusChanged( 705 currentProjectionState, 706 currentProjectionPackageName, 707 projectionStatusDetails)); 708 } 709 } 710 } 711 712 /** 713 * Unregister provided listener from projection status notifications 714 * 715 * @param listener the listener for projection status notifications that was previously 716 * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)} 717 */ 718 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS) 719 @AddedInOrBefore(majorVersion = 33) unregisterProjectionStatusListener(@onNull ProjectionStatusListener listener)720 public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) { 721 Objects.requireNonNull(listener, "listener cannot be null"); 722 synchronized (mLock) { 723 if (!mProjectionStatusListeners.remove(listener) 724 || !mProjectionStatusListeners.isEmpty()) { 725 return; 726 } 727 unregisterProjectionStatusListenerFromCarServiceLocked(); 728 } 729 } 730 unregisterProjectionStatusListenerFromCarServiceLocked()731 private void unregisterProjectionStatusListenerFromCarServiceLocked() { 732 try { 733 mService.unregisterProjectionStatusListener(mCarProjectionStatusListener); 734 mCarProjectionStatusListener = null; 735 } catch (RemoteException e) { 736 handleRemoteExceptionFromCarService(e); 737 } 738 } 739 handleProjectionStatusChanged(@rojectionState int state, String packageName, List<ProjectionStatus> details)740 private void handleProjectionStatusChanged(@ProjectionState int state, 741 String packageName, List<ProjectionStatus> details) { 742 List<ProjectionStatusListener> listeners; 743 synchronized (mLock) { 744 listeners = new ArrayList<>(mProjectionStatusListeners); 745 } 746 for (ProjectionStatusListener listener : listeners) { 747 listener.onProjectionStatusChanged(state, packageName, details); 748 } 749 } 750 751 /** 752 * Returns {@link Bundle} object that contains customization for projection app. This bundle 753 * can be parsed using {@link ProjectionOptions}. 754 */ 755 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 756 @AddedInOrBefore(majorVersion = 33) getProjectionOptions()757 public @NonNull Bundle getProjectionOptions() { 758 try { 759 return mService.getProjectionOptions(); 760 } catch (RemoteException e) { 761 return handleRemoteExceptionFromCarService(e, Bundle.EMPTY); 762 } 763 } 764 765 /** 766 * Resets projection access point credentials if system was configured to persist local-only 767 * hotspot credentials. 768 */ 769 @RequiresPermission(Car.PERMISSION_CAR_PROJECTION) 770 @AddedInOrBefore(majorVersion = 33) resetProjectionAccessPointCredentials()771 public void resetProjectionAccessPointCredentials() { 772 try { 773 mService.resetProjectionAccessPointCredentials(); 774 } catch (RemoteException e) { 775 handleRemoteExceptionFromCarService(e); 776 } 777 } 778 779 /** 780 * Callback class for applications to receive updates about the LocalOnlyHotspot status. 781 */ 782 public abstract static class ProjectionAccessPointCallback { 783 @AddedInOrBefore(majorVersion = 33) 784 public static final int ERROR_NO_CHANNEL = 1; 785 @AddedInOrBefore(majorVersion = 33) 786 public static final int ERROR_GENERIC = 2; 787 @AddedInOrBefore(majorVersion = 33) 788 public static final int ERROR_INCOMPATIBLE_MODE = 3; 789 @AddedInOrBefore(majorVersion = 33) 790 public static final int ERROR_TETHERING_DISALLOWED = 4; 791 792 /** 793 * Called when access point started successfully. 794 * <p> 795 * Note that AP detail may contain configuration which is cannot be represented 796 * by the legacy WifiConfiguration, in such cases a null will be returned. 797 * For example: 798 * <li> SoftAp band in {@link WifiConfiguration.apBand} only supports 799 * 2GHz, 5GHz, 2GHz+5GHz bands, so conversion is limited to these bands. </li> 800 * <li> SoftAp security type in {@link WifiConfiguration.KeyMgmt} only supports 801 * NONE, WPA2_PSK, so conversion is limited to these security type.</li> 802 * 803 * @param wifiConfiguration the {@link WifiConfiguration} of the current hotspot. 804 * @deprecated This callback is deprecated. Use {@link #onStarted(SoftApConfiguration))} 805 * instead. 806 */ 807 @Deprecated 808 @AddedInOrBefore(majorVersion = 33) onStarted(@ullable WifiConfiguration wifiConfiguration)809 public void onStarted(@Nullable WifiConfiguration wifiConfiguration) {} 810 811 /** 812 * Called when access point started successfully. 813 * 814 * @param softApConfiguration the {@link SoftApConfiguration} of the current hotspot. 815 */ 816 @AddedInOrBefore(majorVersion = 33) onStarted(@onNull SoftApConfiguration softApConfiguration)817 public void onStarted(@NonNull SoftApConfiguration softApConfiguration) { 818 onStarted(softApConfiguration.toWifiConfiguration()); 819 } 820 821 /** Called when access point is stopped. No events will be sent after that. */ 822 @AddedInOrBefore(majorVersion = 33) onStopped()823 public void onStopped() {} 824 /** Called when access point failed to start. No events will be sent after that. */ 825 @AddedInOrBefore(majorVersion = 33) onFailed(int reason)826 public void onFailed(int reason) {} 827 } 828 829 /** 830 * Callback proxy for LocalOnlyHotspotCallback objects. 831 */ 832 private static class ProjectionAccessPointCallbackProxy { 833 private static final String LOG_PREFIX = 834 ProjectionAccessPointCallbackProxy.class.getSimpleName() + ": "; 835 836 private final Handler mHandler; 837 private final WeakReference<CarProjectionManager> mCarProjectionManagerRef; 838 private final Messenger mMessenger; 839 ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, final ProjectionAccessPointCallback callback)840 ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, 841 final ProjectionAccessPointCallback callback) { 842 mCarProjectionManagerRef = new WeakReference<>(manager); 843 844 mHandler = new Handler(looper) { 845 @Override 846 public void handleMessage(Message msg) { 847 Log.d(TAG, LOG_PREFIX + "handle message what: " + msg.what + " msg: " + msg); 848 849 CarProjectionManager manager = mCarProjectionManagerRef.get(); 850 if (manager == null) { 851 Log.w(TAG, LOG_PREFIX + "handle message post GC"); 852 return; 853 } 854 855 switch (msg.what) { 856 case PROJECTION_AP_STARTED: 857 if (msg.obj == null) { 858 Log.e(TAG, LOG_PREFIX + "config cannot be null."); 859 callback.onFailed(ProjectionAccessPointCallback.ERROR_GENERIC); 860 return; 861 } 862 if (msg.obj instanceof SoftApConfiguration) { 863 callback.onStarted((SoftApConfiguration) msg.obj); 864 } else if (msg.obj instanceof WifiConfiguration) { 865 callback.onStarted((WifiConfiguration) msg.obj); 866 } 867 break; 868 case PROJECTION_AP_STOPPED: 869 Log.i(TAG, LOG_PREFIX + "hotspot stopped"); 870 callback.onStopped(); 871 break; 872 case PROJECTION_AP_FAILED: 873 int reasonCode = msg.arg1; 874 Log.w(TAG, LOG_PREFIX + "failed to start. reason: " 875 + reasonCode); 876 callback.onFailed(reasonCode); 877 break; 878 default: 879 Log.e(TAG, LOG_PREFIX + "unhandled message. type: " + msg.what); 880 } 881 } 882 }; 883 mMessenger = new Messenger(mHandler); 884 } 885 getMessenger()886 Messenger getMessenger() { 887 return mMessenger; 888 } 889 } 890 891 private static class ICarProjectionKeyEventHandlerImpl 892 extends ICarProjectionKeyEventHandler.Stub { 893 894 private final WeakReference<CarProjectionManager> mManager; 895 ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager)896 private ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager) { 897 mManager = new WeakReference<>(manager); 898 } 899 900 @Override onKeyEvent(@eyEventNum int event)901 public void onKeyEvent(@KeyEventNum int event) { 902 Log.d(TAG, "Received projection key event " + event); 903 final CarProjectionManager manager = mManager.get(); 904 if (manager == null) { 905 return; 906 } 907 908 List<Pair<ProjectionKeyEventHandler, Executor>> toDispatch = new ArrayList<>(); 909 synchronized (manager.mLock) { 910 for (Map.Entry<ProjectionKeyEventHandler, KeyEventHandlerRecord> entry : 911 manager.mKeyEventHandlers.entrySet()) { 912 if (entry.getValue().mSubscribedEvents.get(event)) { 913 toDispatch.add(Pair.create(entry.getKey(), entry.getValue().mExecutor)); 914 } 915 } 916 } 917 918 for (Pair<ProjectionKeyEventHandler, Executor> entry : toDispatch) { 919 ProjectionKeyEventHandler listener = entry.first; 920 entry.second.execute(() -> listener.onKeyEvent(event)); 921 } 922 } 923 } 924 925 private static class KeyEventHandlerRecord { 926 @NonNull Executor mExecutor; 927 @NonNull BitSet mSubscribedEvents; 928 KeyEventHandlerRecord(@onNull Executor executor, @NonNull BitSet subscribedEvents)929 KeyEventHandlerRecord(@NonNull Executor executor, @NonNull BitSet subscribedEvents) { 930 mExecutor = executor; 931 mSubscribedEvents = subscribedEvents; 932 } 933 } 934 935 private static class CarProjectionStatusListenerImpl 936 extends ICarProjectionStatusListener.Stub { 937 938 private @ProjectionState int mCurrentState; 939 private @Nullable String mCurrentPackageName; 940 private List<ProjectionStatus> mDetails = new ArrayList<>(0); 941 942 private final WeakReference<CarProjectionManager> mManagerRef; 943 CarProjectionStatusListenerImpl(CarProjectionManager mgr)944 private CarProjectionStatusListenerImpl(CarProjectionManager mgr) { 945 mManagerRef = new WeakReference<>(mgr); 946 } 947 948 @Override onProjectionStatusChanged(int projectionState, String packageName, List<ProjectionStatus> details)949 public void onProjectionStatusChanged(int projectionState, 950 String packageName, 951 List<ProjectionStatus> details) { 952 CarProjectionManager mgr = mManagerRef.get(); 953 if (mgr != null) { 954 mgr.getEventHandler().post(() -> { 955 mCurrentState = projectionState; 956 mCurrentPackageName = packageName; 957 mDetails = Collections.unmodifiableList(details); 958 959 mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails); 960 }); 961 } 962 } 963 } 964 } 965