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