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