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