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