• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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