• 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 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 implements 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 Handler mHandler;
198     private final Executor mHandlerExecutor;
199 
200     @GuardedBy("mLock")
201     private CarProjectionListener mListener;
202     @GuardedBy("mLock")
203     private int mVoiceSearchFilter;
204     private final ProjectionKeyEventHandler mLegacyListenerTranslator =
205             this::translateKeyEventToLegacyListener;
206 
207     private final ICarProjectionKeyEventHandlerImpl mBinderHandler =
208             new ICarProjectionKeyEventHandlerImpl(this);
209 
210     @GuardedBy("mLock")
211     private final Map<ProjectionKeyEventHandler, KeyEventHandlerRecord> mKeyEventHandlers =
212             new HashMap<>();
213     @GuardedBy("mLock")
214     private BitSet mHandledEvents = new BitSet();
215 
216     private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy;
217 
218     private final Set<ProjectionStatusListener> mProjectionStatusListeners = new LinkedHashSet<>();
219     private CarProjectionStatusListenerImpl mCarProjectionStatusListener;
220 
221     // Only one access point proxy object per process.
222     private static final IBinder mAccessPointProxyToken = new Binder();
223 
224     /**
225      * Interface to receive for projection status updates.
226      */
227     public interface ProjectionStatusListener {
228         /**
229          * This method gets invoked if projection status has been changed.
230          *
231          * @param state - current projection state
232          * @param packageName - if projection is currently running either in the foreground or
233          *                      in the background this argument will contain its package name
234          * @param details - contains detailed information about all currently registered projection
235          *                  receivers.
236          */
onProjectionStatusChanged(@rojectionState int state, @Nullable String packageName, @NonNull List<ProjectionStatus> details)237         void onProjectionStatusChanged(@ProjectionState int state, @Nullable String packageName,
238                 @NonNull List<ProjectionStatus> details);
239     }
240 
241     /**
242      * @hide
243      */
CarProjectionManager(IBinder service, Handler handler)244     public CarProjectionManager(IBinder service, Handler handler) {
245         mService = ICarProjection.Stub.asInterface(service);
246         mHandler = handler;
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             throw e.rethrowFromSystemServer();
452         }
453 
454         mHandledEvents = events;
455     }
456 
457     /**
458      * Registers projection runner on projection start with projection service
459      * to create reverse binding.
460      * @param serviceIntent
461      */
462     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
registerProjectionRunner(@onNull Intent serviceIntent)463     public void registerProjectionRunner(@NonNull Intent serviceIntent) {
464         Preconditions.checkNotNull("serviceIntent cannot be null");
465         synchronized (mLock) {
466             try {
467                 mService.registerProjectionRunner(serviceIntent);
468             } catch (RemoteException e) {
469                 throw e.rethrowFromSystemServer();
470             }
471         }
472     }
473 
474     /**
475      * Unregisters projection runner on projection stop with projection service to create
476      * reverse binding.
477      * @param serviceIntent
478      */
479     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
unregisterProjectionRunner(@onNull Intent serviceIntent)480     public void unregisterProjectionRunner(@NonNull Intent serviceIntent) {
481         Preconditions.checkNotNull("serviceIntent cannot be null");
482         synchronized (mLock) {
483             try {
484                 mService.unregisterProjectionRunner(serviceIntent);
485             } catch (RemoteException e) {
486                 throw e.rethrowFromSystemServer();
487             }
488         }
489     }
490 
491     /** @hide */
492     @Override
onCarDisconnected()493     public void onCarDisconnected() {
494         // nothing to do
495     }
496 
497     /**
498      * Request to start Wi-Fi access point if it hasn't been started yet for wireless projection
499      * receiver app.
500      *
501      * <p>A process can have only one request to start an access point, subsequent call of this
502      * method will invalidate previous calls.
503      *
504      * @param callback to receive notifications when access point status changed for the request
505      */
506     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
startProjectionAccessPoint(@onNull ProjectionAccessPointCallback callback)507     public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) {
508         Preconditions.checkNotNull(callback, "callback cannot be null");
509         synchronized (mLock) {
510             Looper looper = mHandler.getLooper();
511             ProjectionAccessPointCallbackProxy proxy =
512                     new ProjectionAccessPointCallbackProxy(this, looper, callback);
513             try {
514                 mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken);
515                 mProjectionAccessPointCallbackProxy = proxy;
516             } catch (RemoteException e) {
517                 throw e.rethrowFromSystemServer();
518             }
519         }
520     }
521 
522     /**
523      * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz,
524      * e.g. channel 1 will be represented as 2412 in the list.
525      *
526      * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*}
527      */
528     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
getAvailableWifiChannels(int band)529     public @NonNull List<Integer> getAvailableWifiChannels(int band) {
530         try {
531             int[] channels = mService.getAvailableWifiChannels(band);
532             List<Integer> channelList = new ArrayList<>(channels.length);
533             for (int v : channels) {
534                 channelList.add(v);
535             }
536             return channelList;
537         } catch (RemoteException e) {
538             throw e.rethrowFromSystemServer();
539         }
540     }
541 
542     /**
543      * Stop Wi-Fi Access Point for wireless projection receiver app.
544      */
545     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
stopProjectionAccessPoint()546     public void stopProjectionAccessPoint() {
547         ProjectionAccessPointCallbackProxy proxy;
548         synchronized (mLock) {
549             proxy = mProjectionAccessPointCallbackProxy;
550             mProjectionAccessPointCallbackProxy = null;
551         }
552         if (proxy == null) {
553             return;
554         }
555 
556         try {
557             mService.stopProjectionAccessPoint(mAccessPointProxyToken);
558         } catch (RemoteException e) {
559             throw e.rethrowFromSystemServer();
560         }
561     }
562 
563     /**
564      * Request to disconnect the given profile on the given device, and prevent it from reconnecting
565      * until either the request is released, or the process owning the given token dies.
566      *
567      * @param device  The device on which to inhibit a profile.
568      * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit.
569      * @return True if the profile was successfully inhibited, false if an error occurred.
570      */
571     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
requestBluetoothProfileInhibit( @onNull BluetoothDevice device, int profile)572     public boolean requestBluetoothProfileInhibit(
573             @NonNull BluetoothDevice device, int profile) {
574         Preconditions.checkNotNull(device, "device cannot be null");
575         try {
576             return mService.requestBluetoothProfileInhibit(device, profile, mToken);
577         } catch (RemoteException e) {
578             throw e.rethrowFromSystemServer();
579         }
580     }
581 
582     /**
583      * Release an inhibit request made by {@link #requestBluetoothProfileInhibit}, and reconnect the
584      * profile if no other inhibit requests are active.
585      *
586      * @param device  The device on which to release the inhibit request.
587      * @param profile The profile on which to release the inhibit request.
588      * @return True if the request was released, false if an error occurred.
589      */
590     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
releaseBluetoothProfileInhibit(@onNull BluetoothDevice device, int profile)591     public boolean releaseBluetoothProfileInhibit(@NonNull BluetoothDevice device, int profile) {
592         Preconditions.checkNotNull(device, "device cannot be null");
593         try {
594             return mService.releaseBluetoothProfileInhibit(device, profile, mToken);
595         } catch (RemoteException e) {
596             throw e.rethrowFromSystemServer();
597         }
598     }
599 
600     /**
601      * Call this method to report projection status of your app. The aggregated status (from other
602      * projection apps if available) will be broadcasted to interested parties.
603      *
604      * @param status the reported status that will be distributed to the interested listeners
605      *
606      * @see #registerProjectionStatusListener(ProjectionStatusListener)
607      */
608     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
updateProjectionStatus(@onNull ProjectionStatus status)609     public void updateProjectionStatus(@NonNull ProjectionStatus status) {
610         Preconditions.checkNotNull(status, "status cannot be null");
611         try {
612             mService.updateProjectionStatus(status, mToken);
613         } catch (RemoteException e) {
614             throw e.rethrowFromSystemServer();
615         }
616     }
617 
618     /**
619      * Register projection status listener. See {@link ProjectionStatusListener} for details. It is
620      * allowed to register multiple listeners.
621      *
622      * <p>Note: provided listener will be called immediately with the most recent status.
623      *
624      * @param listener the listener to receive notification for any projection status changes
625      */
626     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
registerProjectionStatusListener(@onNull ProjectionStatusListener listener)627     public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
628         Preconditions.checkNotNull(listener, "listener cannot be null");
629         synchronized (mLock) {
630             mProjectionStatusListeners.add(listener);
631 
632             if (mCarProjectionStatusListener == null) {
633                 mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this);
634                 try {
635                     mService.registerProjectionStatusListener(mCarProjectionStatusListener);
636                 } catch (RemoteException e) {
637                     throw e.rethrowFromSystemServer();
638                 }
639             } else {
640                 // Already subscribed to Car Service, immediately notify listener with the current
641                 // projection status in the event handler thread.
642                 mHandler.post(() ->
643                         listener.onProjectionStatusChanged(
644                                 mCarProjectionStatusListener.mCurrentState,
645                                 mCarProjectionStatusListener.mCurrentPackageName,
646                                 mCarProjectionStatusListener.mDetails));
647             }
648         }
649     }
650 
651     /**
652      * Unregister provided listener from projection status notifications
653      *
654      * @param listener the listener for projection status notifications that was previously
655      * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)}
656      */
657     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
unregisterProjectionStatusListener(@onNull ProjectionStatusListener listener)658     public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
659         Preconditions.checkNotNull(listener, "listener cannot be null");
660         synchronized (mLock) {
661             if (!mProjectionStatusListeners.remove(listener)
662                     || !mProjectionStatusListeners.isEmpty()) {
663                 return;
664             }
665             unregisterProjectionStatusListenerFromCarServiceLocked();
666         }
667     }
668 
unregisterProjectionStatusListenerFromCarServiceLocked()669     private void unregisterProjectionStatusListenerFromCarServiceLocked() {
670         try {
671             mService.unregisterProjectionStatusListener(mCarProjectionStatusListener);
672             mCarProjectionStatusListener = null;
673         } catch (RemoteException e) {
674             throw e.rethrowFromSystemServer();
675         }
676     }
677 
handleProjectionStatusChanged(@rojectionState int state, String packageName, List<ProjectionStatus> details)678     private void handleProjectionStatusChanged(@ProjectionState int state,
679             String packageName, List<ProjectionStatus> details) {
680         List<ProjectionStatusListener> listeners;
681         synchronized (mLock) {
682             listeners = new ArrayList<>(mProjectionStatusListeners);
683         }
684         for (ProjectionStatusListener listener : listeners) {
685             listener.onProjectionStatusChanged(state, packageName, details);
686         }
687     }
688 
689     /**
690      * Returns {@link Bundle} object that contains customization for projection app. This bundle
691      * can be parsed using {@link ProjectionOptions}.
692      */
693     @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
getProjectionOptions()694     public @NonNull Bundle getProjectionOptions() {
695         try {
696             return mService.getProjectionOptions();
697         } catch (RemoteException e) {
698             throw e.rethrowFromSystemServer();
699         }
700     }
701 
702     /**
703      * Callback class for applications to receive updates about the LocalOnlyHotspot status.
704      */
705     public abstract static class ProjectionAccessPointCallback {
706         public static final int ERROR_NO_CHANNEL = 1;
707         public static final int ERROR_GENERIC = 2;
708         public static final int ERROR_INCOMPATIBLE_MODE = 3;
709         public static final int ERROR_TETHERING_DISALLOWED = 4;
710 
711         /** Called when access point started successfully. */
onStarted(WifiConfiguration wifiConfiguration)712         public void onStarted(WifiConfiguration wifiConfiguration) {}
713         /** Called when access point is stopped. No events will be sent after that. */
onStopped()714         public void onStopped() {}
715         /** Called when access point failed to start. No events will be sent after that. */
onFailed(int reason)716         public void onFailed(int reason) {}
717     }
718 
719     /**
720      * Callback proxy for LocalOnlyHotspotCallback objects.
721      */
722     private static class ProjectionAccessPointCallbackProxy {
723         private static final String LOG_PREFIX =
724                 ProjectionAccessPointCallbackProxy.class.getSimpleName() + ": ";
725 
726         private final Handler mHandler;
727         private final WeakReference<CarProjectionManager> mCarProjectionManagerRef;
728         private final Messenger mMessenger;
729 
ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper, final ProjectionAccessPointCallback callback)730         ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper,
731                 final ProjectionAccessPointCallback callback) {
732             mCarProjectionManagerRef = new WeakReference<>(manager);
733 
734             mHandler = new Handler(looper) {
735                 @Override
736                 public void handleMessage(Message msg) {
737                     Log.d(TAG, LOG_PREFIX + "handle message what: " + msg.what + " msg: " + msg);
738 
739                     CarProjectionManager manager = mCarProjectionManagerRef.get();
740                     if (manager == null) {
741                         Log.w(TAG, LOG_PREFIX + "handle message post GC");
742                         return;
743                     }
744 
745                     switch (msg.what) {
746                         case PROJECTION_AP_STARTED:
747                             WifiConfiguration config = (WifiConfiguration) msg.obj;
748                             if (config == null) {
749                                 Log.e(TAG, LOG_PREFIX + "config cannot be null.");
750                                 callback.onFailed(ProjectionAccessPointCallback.ERROR_GENERIC);
751                                 return;
752                             }
753                             callback.onStarted(config);
754                             break;
755                         case PROJECTION_AP_STOPPED:
756                             Log.i(TAG, LOG_PREFIX + "hotspot stopped");
757                             callback.onStopped();
758                             break;
759                         case PROJECTION_AP_FAILED:
760                             int reasonCode = msg.arg1;
761                             Log.w(TAG, LOG_PREFIX + "failed to start.  reason: "
762                                     + reasonCode);
763                             callback.onFailed(reasonCode);
764                             break;
765                         default:
766                             Log.e(TAG, LOG_PREFIX + "unhandled message.  type: " + msg.what);
767                     }
768                 }
769             };
770             mMessenger = new Messenger(mHandler);
771         }
772 
getMessenger()773         Messenger getMessenger() {
774             return mMessenger;
775         }
776     }
777 
778     private static class ICarProjectionKeyEventHandlerImpl
779             extends ICarProjectionKeyEventHandler.Stub {
780 
781         private final WeakReference<CarProjectionManager> mManager;
782 
ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager)783         private ICarProjectionKeyEventHandlerImpl(CarProjectionManager manager) {
784             mManager = new WeakReference<>(manager);
785         }
786 
787         @Override
onKeyEvent(@eyEventNum int event)788         public void onKeyEvent(@KeyEventNum int event) {
789             Log.d(TAG, "Received projection key event " + event);
790             final CarProjectionManager manager = mManager.get();
791             if (manager == null) {
792                 return;
793             }
794 
795             List<Pair<ProjectionKeyEventHandler, Executor>> toDispatch = new ArrayList<>();
796             synchronized (manager.mLock) {
797                 for (Map.Entry<ProjectionKeyEventHandler, KeyEventHandlerRecord> entry :
798                         manager.mKeyEventHandlers.entrySet()) {
799                     if (entry.getValue().mSubscribedEvents.get(event)) {
800                         toDispatch.add(Pair.create(entry.getKey(), entry.getValue().mExecutor));
801                     }
802                 }
803             }
804 
805             for (Pair<ProjectionKeyEventHandler, Executor> entry : toDispatch) {
806                 ProjectionKeyEventHandler listener = entry.first;
807                 entry.second.execute(() -> listener.onKeyEvent(event));
808             }
809         }
810     }
811 
812     private static class KeyEventHandlerRecord {
813         @NonNull Executor mExecutor;
814         @NonNull BitSet mSubscribedEvents;
815 
KeyEventHandlerRecord(@onNull Executor executor, @NonNull BitSet subscribedEvents)816         KeyEventHandlerRecord(@NonNull Executor executor, @NonNull BitSet subscribedEvents) {
817             mExecutor = executor;
818             mSubscribedEvents = subscribedEvents;
819         }
820     }
821 
822     private static class CarProjectionStatusListenerImpl
823             extends ICarProjectionStatusListener.Stub {
824 
825         private @ProjectionState int mCurrentState;
826         private @Nullable String mCurrentPackageName;
827         private List<ProjectionStatus> mDetails = new ArrayList<>(0);
828 
829         private final WeakReference<CarProjectionManager> mManagerRef;
830 
CarProjectionStatusListenerImpl(CarProjectionManager mgr)831         private CarProjectionStatusListenerImpl(CarProjectionManager mgr) {
832             mManagerRef = new WeakReference<>(mgr);
833         }
834 
835         @Override
onProjectionStatusChanged(int projectionState, String packageName, List<ProjectionStatus> details)836         public void onProjectionStatusChanged(int projectionState,
837                 String packageName,
838                 List<ProjectionStatus> details) {
839             CarProjectionManager mgr = mManagerRef.get();
840             if (mgr != null) {
841                 mgr.mHandler.post(() -> {
842                     mCurrentState = projectionState;
843                     mCurrentPackageName = packageName;
844                     mDetails = Collections.unmodifiableList(details);
845 
846                     mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails);
847                 });
848             }
849         }
850     }
851 }
852