• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package com.android.car;
17 
18 import static android.car.CarProjectionManager.ProjectionAccessPointCallback.ERROR_GENERIC;
19 import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
20 import static android.car.projection.ProjectionStatus.PROJECTION_STATE_READY_TO_PROJECT;
21 import static android.net.wifi.WifiManager.EXTRA_PREVIOUS_WIFI_AP_STATE;
22 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_FAILURE_REASON;
23 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
24 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
25 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
26 import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
27 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
28 import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLING;
29 
30 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
31 
32 import android.annotation.Nullable;
33 import android.app.ActivityOptions;
34 import android.bluetooth.BluetoothDevice;
35 import android.car.CarProjectionManager;
36 import android.car.CarProjectionManager.ProjectionAccessPointCallback;
37 import android.car.ICarProjection;
38 import android.car.ICarProjectionKeyEventHandler;
39 import android.car.ICarProjectionStatusListener;
40 import android.car.builtin.content.pm.PackageManagerHelper;
41 import android.car.builtin.util.Slogf;
42 import android.car.projection.ProjectionOptions;
43 import android.car.projection.ProjectionStatus;
44 import android.car.projection.ProjectionStatus.ProjectionState;
45 import android.content.BroadcastReceiver;
46 import android.content.ComponentName;
47 import android.content.Context;
48 import android.content.Intent;
49 import android.content.IntentFilter;
50 import android.content.ServiceConnection;
51 import android.content.SharedPreferences;
52 import android.content.pm.PackageManager;
53 import android.content.res.Resources;
54 import android.graphics.Rect;
55 import android.net.MacAddress;
56 import android.net.wifi.SoftApConfiguration;
57 import android.net.wifi.WifiClient;
58 import android.net.wifi.WifiManager;
59 import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
60 import android.net.wifi.WifiManager.LocalOnlyHotspotReservation;
61 import android.net.wifi.WifiScanner;
62 import android.os.Binder;
63 import android.os.Bundle;
64 import android.os.Handler;
65 import android.os.IBinder;
66 import android.os.Message;
67 import android.os.Messenger;
68 import android.os.RemoteException;
69 import android.os.UserHandle;
70 import android.text.TextUtils;
71 
72 import com.android.car.BinderInterfaceContainer.BinderInterface;
73 import com.android.car.bluetooth.CarBluetoothService;
74 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
75 import com.android.car.internal.os.HandlerExecutor;
76 import com.android.car.internal.util.IndentingPrintWriter;
77 import com.android.internal.annotations.GuardedBy;
78 import com.android.internal.annotations.VisibleForTesting;
79 import com.android.internal.util.Preconditions;
80 
81 import java.lang.ref.WeakReference;
82 import java.net.NetworkInterface;
83 import java.net.SocketException;
84 import java.util.ArrayList;
85 import java.util.BitSet;
86 import java.util.HashMap;
87 import java.util.List;
88 import java.util.Optional;
89 
90 /**
91  * Car projection service allows to bound to projected app to boost it priority.
92  * It also enables projected applications to handle voice action requests.
93  */
94 class CarProjectionService extends ICarProjection.Stub implements CarServiceBase,
95         BinderInterfaceContainer.BinderEventHandler<ICarProjectionKeyEventHandler>,
96         CarProjectionManager.ProjectionKeyEventHandler {
97     private static final String TAG = CarLog.tagFor(CarProjectionService.class);
98     private static final boolean DBG = true;
99 
100     private final CarInputService mCarInputService;
101     private final CarBluetoothService mCarBluetoothService;
102     private final Context mContext;
103     private final WifiManager mWifiManager;
104     private final Handler mHandler;
105     private final Object mLock = new Object();
106 
107     @GuardedBy("mLock")
108     private final HashMap<IBinder, WirelessClient> mWirelessClients = new HashMap<>();
109 
110     @GuardedBy("mLock")
111     private @Nullable LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
112 
113     @GuardedBy("mLock")
114     private @Nullable ProjectionSoftApCallback mSoftApCallback;
115 
116     @GuardedBy("mLock")
117     private final HashMap<IBinder, ProjectionReceiverClient> mProjectionReceiverClients =
118             new HashMap<>();
119 
120     @Nullable
121     private MacAddress mApBssid;
122 
123     @GuardedBy("mLock")
124     private @Nullable WifiScanner mWifiScanner;
125 
126     @GuardedBy("mLock")
127     private @ProjectionState int mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
128 
129     @GuardedBy("mLock")
130     private ProjectionOptions mProjectionOptions;
131 
132     @GuardedBy("mLock")
133     private @Nullable String mCurrentProjectionPackage;
134 
135     private final BinderInterfaceContainer<ICarProjectionStatusListener>
136             mProjectionStatusListeners = new BinderInterfaceContainer<>();
137 
138     @GuardedBy("mLock")
139     private final ProjectionKeyEventHandlerContainer mKeyEventHandlers;
140 
141     @GuardedBy("mLock")
142     private @Nullable SoftApConfiguration mApConfiguration;
143 
144     private static final String SHARED_PREF_NAME = "com.android.car.car_projection_service";
145     private static final String KEY_AP_CONFIG_SSID = "ap_config_ssid";
146     private static final String KEY_AP_CONFIG_BSSID = "ap_config_bssid";
147     private static final String KEY_AP_CONFIG_PASSPHRASE = "ap_config_passphrase";
148     private static final String KEY_AP_CONFIG_SECURITY_TYPE = "ap_config_security_type";
149 
150     private static final int WIFI_MODE_TETHERED = 1;
151     private static final int WIFI_MODE_LOCALONLY = 2;
152 
153     // Could be one of the WIFI_MODE_* constants.
154     // TODO: read this from user settings, support runtime switch
155     private int mWifiMode;
156 
157     private boolean mStableLocalOnlyHotspotConfig;
158 
159     private final ServiceConnection mConnection = new ServiceConnection() {
160             @Override
161             public void onServiceConnected(ComponentName className, IBinder service) {
162                 synchronized (mLock) {
163                     mBound = true;
164                 }
165             }
166 
167             @Override
168             public void onServiceDisconnected(ComponentName className) {
169                 // Service has crashed.
170                 Slogf.w(CarLog.TAG_PROJECTION, "Service disconnected: " + className);
171                 synchronized (mLock) {
172                     mRegisteredService = null;
173                 }
174                 unbindServiceIfBound();
175             }
176         };
177 
178     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
179         @Override
180         public void onReceive(Context context, Intent intent) {
181             int currState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED);
182             int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE,
183                     WIFI_AP_STATE_DISABLED);
184             int errorCode = intent.getIntExtra(EXTRA_WIFI_AP_FAILURE_REASON, 0);
185             String ifaceName = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
186             int mode = intent.getIntExtra(EXTRA_WIFI_AP_MODE,
187                     WifiManager.IFACE_IP_MODE_UNSPECIFIED);
188             handleWifiApStateChange(currState, prevState, errorCode, ifaceName, mode);
189         }
190     };
191 
192     private boolean mBound;
193     private Intent mRegisteredService;
194 
CarProjectionService(Context context, @Nullable Handler handler, CarInputService carInputService, CarBluetoothService carBluetoothService)195     CarProjectionService(Context context, @Nullable Handler handler,
196             CarInputService carInputService, CarBluetoothService carBluetoothService) {
197         mContext = context;
198         mHandler = handler == null ? new Handler() : handler;
199         mCarInputService = carInputService;
200         mCarBluetoothService = carBluetoothService;
201         mKeyEventHandlers = new ProjectionKeyEventHandlerContainer(this);
202         mWifiManager = context.getSystemService(WifiManager.class);
203 
204         final Resources res = mContext.getResources();
205         setAccessPointTethering(res.getBoolean(R.bool.config_projectionAccessPointTethering));
206         setStableLocalOnlyHotspotConfig(
207                 res.getBoolean(R.bool.config_stableLocalOnlyHotspotConfig));
208     }
209 
210     @Override
registerProjectionRunner(Intent serviceIntent)211     public void registerProjectionRunner(Intent serviceIntent) {
212         CarServiceUtils.assertProjectionPermission(mContext);
213         // We assume one active projection app running in the system at one time.
214         synchronized (mLock) {
215             if (serviceIntent.filterEquals(mRegisteredService) && mBound) {
216                 return;
217             }
218             if (mRegisteredService != null) {
219                 Slogf.w(CarLog.TAG_PROJECTION, "Registering new service[" + serviceIntent
220                         + "] while old service[" + mRegisteredService + "] is still running");
221             }
222             unbindServiceIfBound();
223         }
224         bindToService(serviceIntent);
225     }
226 
227     @Override
unregisterProjectionRunner(Intent serviceIntent)228     public void unregisterProjectionRunner(Intent serviceIntent) {
229         CarServiceUtils.assertProjectionPermission(mContext);
230         synchronized (mLock) {
231             if (!serviceIntent.filterEquals(mRegisteredService)) {
232                 Slogf.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service["
233                         + serviceIntent + "]. Registered service[" + mRegisteredService + "]");
234                 return;
235             }
236             mRegisteredService = null;
237         }
238         unbindServiceIfBound();
239     }
240 
bindToService(Intent serviceIntent)241     private void bindToService(Intent serviceIntent) {
242         synchronized (mLock) {
243             mRegisteredService = serviceIntent;
244         }
245         UserHandle userHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid());
246         mContext.bindServiceAsUser(serviceIntent, mConnection, Context.BIND_AUTO_CREATE,
247                 userHandle);
248     }
249 
unbindServiceIfBound()250     private void unbindServiceIfBound() {
251         synchronized (mLock) {
252             if (!mBound) {
253                 return;
254             }
255             mBound = false;
256             mRegisteredService = null;
257         }
258         mContext.unbindService(mConnection);
259     }
260 
261     @Override
registerKeyEventHandler( ICarProjectionKeyEventHandler eventHandler, byte[] eventMask)262     public void registerKeyEventHandler(
263             ICarProjectionKeyEventHandler eventHandler, byte[] eventMask) {
264         CarServiceUtils.assertProjectionPermission(mContext);
265         BitSet events = BitSet.valueOf(eventMask);
266         Preconditions.checkArgument(
267                 events.length() <= CarProjectionManager.NUM_KEY_EVENTS,
268                 "Unknown handled event");
269         synchronized (mLock) {
270             ProjectionKeyEventHandler info = mKeyEventHandlers.get(eventHandler);
271             if (info == null) {
272                 info = new ProjectionKeyEventHandler(mKeyEventHandlers, eventHandler, events);
273                 mKeyEventHandlers.addBinderInterface(info);
274             } else {
275                 info.setHandledEvents(events);
276             }
277 
278             updateInputServiceHandlerLocked();
279         }
280     }
281 
282     @Override
unregisterKeyEventHandler(ICarProjectionKeyEventHandler eventHandler)283     public void unregisterKeyEventHandler(ICarProjectionKeyEventHandler eventHandler) {
284         CarServiceUtils.assertProjectionPermission(mContext);
285         synchronized (mLock) {
286             mKeyEventHandlers.removeBinder(eventHandler);
287             updateInputServiceHandlerLocked();
288         }
289     }
290 
291     @Override
startProjectionAccessPoint(final Messenger messenger, IBinder binder)292     public void startProjectionAccessPoint(final Messenger messenger, IBinder binder)
293             throws RemoteException {
294         CarServiceUtils.assertProjectionPermission(mContext);
295         //TODO: check if access point already started with the desired configuration.
296         registerWirelessClient(WirelessClient.of(messenger, binder));
297         startAccessPoint();
298     }
299 
300     @Override
stopProjectionAccessPoint(IBinder token)301     public void stopProjectionAccessPoint(IBinder token) {
302         CarServiceUtils.assertProjectionPermission(mContext);
303         Slogf.i(TAG, "Received stop access point request from " + token);
304 
305         boolean shouldReleaseAp;
306         synchronized (mLock) {
307             if (!unregisterWirelessClientLocked(token)) {
308                 Slogf.w(TAG, "Client " + token + " was not registered");
309                 return;
310             }
311             shouldReleaseAp = mWirelessClients.isEmpty();
312         }
313 
314         if (shouldReleaseAp) {
315             stopAccessPoint();
316         }
317     }
318 
319     @Override
getAvailableWifiChannels(int band)320     public int[] getAvailableWifiChannels(int band) {
321         CarServiceUtils.assertProjectionPermission(mContext);
322         WifiScanner scanner;
323         synchronized (mLock) {
324             // Lazy initialization
325             if (mWifiScanner == null) {
326                 mWifiScanner = mContext.getSystemService(WifiScanner.class);
327             }
328             scanner = mWifiScanner;
329         }
330         if (scanner == null) {
331             Slogf.w(TAG, "Unable to get WifiScanner");
332             return new int[0];
333         }
334 
335         List<Integer> channels = scanner.getAvailableChannels(band);
336         if (channels == null || channels.isEmpty()) {
337             Slogf.w(TAG, "WifiScanner reported no available channels");
338             return new int[0];
339         }
340 
341         int[] array = new int[channels.size()];
342         for (int i = 0; i < channels.size(); i++) {
343             array[i] = channels.get(i);
344         }
345         return array;
346     }
347 
348     /**
349      * Request to disconnect the given profile on the given device, and prevent it from reconnecting
350      * until either the request is released, or the process owning the given token dies.
351      *
352      * @param device  The device on which to inhibit a profile.
353      * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit.
354      * @param token   A {@link IBinder} to be used as an identity for the request. If the process
355      *                owning the token dies, the request will automatically be released.
356      * @return True if the profile was successfully inhibited, false if an error occurred.
357      */
358     @Override
requestBluetoothProfileInhibit( BluetoothDevice device, int profile, IBinder token)359     public boolean requestBluetoothProfileInhibit(
360             BluetoothDevice device, int profile, IBinder token) {
361         if (DBG) {
362             Slogf.d(TAG, "requestBluetoothProfileInhibit device=" + device + " profile=" + profile
363                     + " from uid " + Binder.getCallingUid());
364         }
365         CarServiceUtils.assertProjectionPermission(mContext);
366         try {
367             if (device == null) {
368                 // Will be caught by AIDL and thrown to caller.
369                 throw new NullPointerException("Device must not be null");
370             }
371             if (token == null) {
372                 throw new NullPointerException("Token must not be null");
373             }
374             return mCarBluetoothService.requestProfileInhibit(device, profile, token);
375         } catch (RuntimeException e) {
376             Slogf.e(TAG, "Error in requestBluetoothProfileInhibit", e);
377             throw e;
378         }
379     }
380 
381     /**
382      * Release an inhibit request made by {@link #requestBluetoothProfileInhibit}, and reconnect the
383      * profile if no other inhibit requests are active.
384      *
385      * @param device  The device on which to release the inhibit request.
386      * @param profile The profile on which to release the inhibit request.
387      * @param token   The token provided in the original call to
388      *                {@link #requestBluetoothProfileInhibit}.
389      * @return True if the request was released, false if an error occurred.
390      */
391     @Override
releaseBluetoothProfileInhibit( BluetoothDevice device, int profile, IBinder token)392     public boolean releaseBluetoothProfileInhibit(
393             BluetoothDevice device, int profile, IBinder token) {
394         if (DBG) {
395             Slogf.d(TAG, "releaseBluetoothProfileInhibit device=" + device + " profile=" + profile
396                     + " from uid " + Binder.getCallingUid());
397         }
398         CarServiceUtils.assertProjectionPermission(mContext);
399         try {
400             if (device == null) {
401                 // Will be caught by AIDL and thrown to caller.
402                 throw new NullPointerException("Device must not be null");
403             }
404             if (token == null) {
405                 throw new NullPointerException("Token must not be null");
406             }
407             return mCarBluetoothService.releaseProfileInhibit(device, profile, token);
408         } catch (RuntimeException e) {
409             Slogf.e(TAG, "Error in releaseBluetoothProfileInhibit", e);
410             throw e;
411         }
412     }
413 
414     @Override
updateProjectionStatus(ProjectionStatus status, IBinder token)415     public void updateProjectionStatus(ProjectionStatus status, IBinder token)
416             throws RemoteException {
417         if (DBG) {
418             Slogf.d(TAG, "updateProjectionStatus, status: " + status + ", token: " + token);
419         }
420         CarServiceUtils.assertProjectionPermission(mContext);
421         final String packageName = status.getPackageName();
422         final int callingUid = Binder.getCallingUid();
423         final int userHandleId = Binder.getCallingUserHandle().getIdentifier();
424         final int packageUid;
425 
426         try {
427             packageUid = PackageManagerHelper.getPackageUidAsUser(mContext.getPackageManager(),
428                     packageName, userHandleId);
429         } catch (PackageManager.NameNotFoundException e) {
430             throw new SecurityException("Package " + packageName + " does not exist", e);
431         }
432 
433         if (callingUid != packageUid) {
434             throw new SecurityException(
435                     "UID " + callingUid + " cannot update status for package " + packageName);
436         }
437 
438         synchronized (mLock) {
439             ProjectionReceiverClient client = getOrCreateProjectionReceiverClientLocked(token);
440             client.mProjectionStatus = status;
441 
442             // If the projection package that's reporting its projection state is the currently
443             // active projection package, update the state. If it is a different package, update the
444             // current projection state if the new package is reporting that it is projecting or if
445             // it is reporting that it's ready to project, and the current package has an inactive
446             // projection state.
447             if (status.isActive()
448                     || (status.getState() == PROJECTION_STATE_READY_TO_PROJECT
449                             && mCurrentProjectionState == PROJECTION_STATE_INACTIVE)
450                     || TextUtils.equals(packageName, mCurrentProjectionPackage)) {
451                 mCurrentProjectionState = status.getState();
452                 mCurrentProjectionPackage = packageName;
453             }
454         }
455         notifyProjectionStatusChanged(null /* notify all listeners */);
456     }
457 
458     @Override
registerProjectionStatusListener(ICarProjectionStatusListener listener)459     public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
460             throws RemoteException {
461         CarServiceUtils.assertProjectionStatusPermission(mContext);
462         mProjectionStatusListeners.addBinder(listener);
463 
464         // Immediately notify listener with the current status.
465         notifyProjectionStatusChanged(listener);
466     }
467 
468     @Override
unregisterProjectionStatusListener(ICarProjectionStatusListener listener)469     public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
470             throws RemoteException {
471         CarServiceUtils.assertProjectionStatusPermission(mContext);
472         mProjectionStatusListeners.removeBinder(listener);
473     }
474 
475     @GuardedBy("mLock")
getOrCreateProjectionReceiverClientLocked( IBinder token)476     private ProjectionReceiverClient getOrCreateProjectionReceiverClientLocked(
477             IBinder token) throws RemoteException {
478         ProjectionReceiverClient client;
479         client = mProjectionReceiverClients.get(token);
480         if (client == null) {
481             client = new ProjectionReceiverClient(() -> unregisterProjectionReceiverClient(token));
482             token.linkToDeath(client.mDeathRecipient, 0 /* flags */);
483             mProjectionReceiverClients.put(token, client);
484         }
485         return client;
486     }
487 
unregisterProjectionReceiverClient(IBinder token)488     private void unregisterProjectionReceiverClient(IBinder token) {
489         synchronized (mLock) {
490             ProjectionReceiverClient client = mProjectionReceiverClients.remove(token);
491             if (client == null) {
492                 Slogf.w(TAG, "Projection receiver client for token " + token + " doesn't exist");
493                 return;
494             }
495             token.unlinkToDeath(client.mDeathRecipient, 0);
496             if (TextUtils.equals(
497                     client.mProjectionStatus.getPackageName(), mCurrentProjectionPackage)) {
498                 mCurrentProjectionPackage = null;
499                 mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
500             }
501         }
502     }
503 
notifyProjectionStatusChanged( @ullable ICarProjectionStatusListener singleListenerToNotify)504     private void notifyProjectionStatusChanged(
505             @Nullable ICarProjectionStatusListener singleListenerToNotify)
506             throws RemoteException {
507         int currentState;
508         String currentPackage;
509         List<ProjectionStatus> statuses = new ArrayList<>();
510         synchronized (mLock) {
511             for (ProjectionReceiverClient client : mProjectionReceiverClients.values()) {
512                 statuses.add(client.mProjectionStatus);
513             }
514             currentState = mCurrentProjectionState;
515             currentPackage = mCurrentProjectionPackage;
516         }
517 
518         if (DBG) {
519             Slogf.d(TAG, "Notify projection status change, state: " + currentState + ", pkg: "
520                     + currentPackage + ", listeners: " + mProjectionStatusListeners.size()
521                     + ", listenerToNotify: " + singleListenerToNotify);
522         }
523 
524         if (singleListenerToNotify == null) {
525             for (BinderInterface<ICarProjectionStatusListener> listener :
526                     mProjectionStatusListeners.getInterfaces()) {
527                 try {
528                     listener.binderInterface.onProjectionStatusChanged(
529                             currentState, currentPackage, statuses);
530                 } catch (RemoteException ex) {
531                     Slogf.e(TAG, "Error calling to projection status listener", ex);
532                 }
533             }
534         } else {
535             singleListenerToNotify.onProjectionStatusChanged(
536                     currentState, currentPackage, statuses);
537         }
538     }
539 
540     @Override
getProjectionOptions()541     public Bundle getProjectionOptions() {
542         CarServiceUtils.assertProjectionPermission(mContext);
543         synchronized (mLock) {
544             if (mProjectionOptions == null) {
545                 mProjectionOptions = createProjectionOptionsBuilder()
546                         .build();
547             }
548             return mProjectionOptions.toBundle();
549         }
550     }
551 
createProjectionOptionsBuilder()552     private ProjectionOptions.Builder createProjectionOptionsBuilder() {
553         Resources res = mContext.getResources();
554 
555         ProjectionOptions.Builder builder = ProjectionOptions.builder();
556 
557         ActivityOptions activityOptions = createActivityOptions(res);
558         if (activityOptions != null) {
559             builder.setProjectionActivityOptions(activityOptions);
560         }
561 
562         String consentActivity = res.getString(R.string.config_projectionConsentActivity);
563         if (!TextUtils.isEmpty(consentActivity)) {
564             builder.setConsentActivity(ComponentName.unflattenFromString(consentActivity));
565         }
566 
567         builder.setUiMode(res.getInteger(R.integer.config_projectionUiMode));
568 
569         int apMode = ProjectionOptions.AP_MODE_NOT_SPECIFIED;
570         if (mWifiMode == WIFI_MODE_TETHERED) {
571             apMode = ProjectionOptions.AP_MODE_TETHERED;
572         } else if (mWifiMode == WIFI_MODE_LOCALONLY) {
573             apMode = mStableLocalOnlyHotspotConfig
574                     ? ProjectionOptions.AP_MODE_LOHS_STATIC_CREDENTIALS
575                     : ProjectionOptions.AP_MODE_LOHS_DYNAMIC_CREDENTIALS;
576         }
577         builder.setAccessPointMode(apMode);
578 
579         return builder;
580     }
581 
582     @Nullable
createActivityOptions(Resources res)583     private static ActivityOptions createActivityOptions(Resources res) {
584         ActivityOptions activityOptions = ActivityOptions.makeBasic();
585         boolean changed = false;
586         int displayId = res.getInteger(R.integer.config_projectionActivityDisplayId);
587         if (displayId != -1) {
588             activityOptions.setLaunchDisplayId(displayId);
589             changed = true;
590         }
591         int[] rawBounds = res.getIntArray(R.array.config_projectionActivityLaunchBounds);
592         if (rawBounds != null && rawBounds.length == 4) {
593             Rect bounds = new Rect(rawBounds[0], rawBounds[1], rawBounds[2], rawBounds[3]);
594             activityOptions.setLaunchBounds(bounds);
595             changed = true;
596         }
597         return changed ? activityOptions : null;
598     }
599 
startAccessPoint()600     private void startAccessPoint() {
601         synchronized (mLock) {
602             switch (mWifiMode) {
603                 case WIFI_MODE_LOCALONLY: {
604                     startLocalOnlyApLocked();
605                     break;
606                 }
607                 case WIFI_MODE_TETHERED: {
608                     startTetheredApLocked();
609                     break;
610                 }
611                 default: {
612                     Slogf.wtf(TAG, "Unexpected Access Point mode during starting: " + mWifiMode);
613                     break;
614                 }
615             }
616         }
617     }
618 
stopAccessPoint()619     private void stopAccessPoint() {
620         sendApStopped();
621 
622         synchronized (mLock) {
623             switch (mWifiMode) {
624                 case WIFI_MODE_LOCALONLY: {
625                     stopLocalOnlyApLocked();
626                     break;
627                 }
628                 case WIFI_MODE_TETHERED: {
629                     stopTetheredApLocked();
630                     break;
631                 }
632                 default: {
633                     Slogf.wtf(TAG, "Unexpected Access Point mode during stopping : " + mWifiMode);
634                 }
635             }
636         }
637     }
638 
639     @GuardedBy("mLock")
startTetheredApLocked()640     private void startTetheredApLocked() {
641         Slogf.d(TAG, "startTetheredApLocked");
642 
643         if (mSoftApCallback == null) {
644             mSoftApCallback = new ProjectionSoftApCallback();
645             mWifiManager.registerSoftApCallback(new HandlerExecutor(mHandler), mSoftApCallback);
646             ensureApConfiguration();
647         }
648 
649         if (!mWifiManager.startTetheredHotspot(null /* use existing config*/)) {
650             // The indicates that AP might be already started.
651             if (mWifiManager.getWifiApState() == WIFI_AP_STATE_ENABLED) {
652                 sendApStarted(mWifiManager.getSoftApConfiguration());
653             } else {
654                 Slogf.e(TAG, "Failed to start soft AP");
655                 sendApFailed(ERROR_GENERIC);
656             }
657         }
658     }
659 
660     @GuardedBy("mLock")
stopTetheredApLocked()661     private void stopTetheredApLocked() {
662         Slogf.d(TAG, "stopTetheredAp");
663 
664         if (mSoftApCallback != null) {
665             mWifiManager.unregisterSoftApCallback(mSoftApCallback);
666             mSoftApCallback = null;
667             if (!mWifiManager.stopSoftAp()) {
668                 Slogf.w(TAG, "Failed to request soft AP to stop.");
669             }
670         }
671     }
672 
673     @Override
resetProjectionAccessPointCredentials()674     public void resetProjectionAccessPointCredentials() {
675         CarServiceUtils.assertProjectionPermission(mContext);
676 
677         if (!mStableLocalOnlyHotspotConfig) {
678             Slogf.i(TAG, "Resetting local-only hotspot credentials ignored as credentials do"
679                     + " not persist.");
680             return;
681         }
682 
683         Slogf.i(TAG, "Clearing local-only hotspot credentials.");
684         getSharedPreferences()
685                 .edit()
686                 .clear()
687                 .apply();
688 
689         synchronized (mLock) {
690             mApConfiguration = null;
691         }
692     }
693 
694     @GuardedBy("mLock")
startLocalOnlyApLocked()695     private void startLocalOnlyApLocked() {
696         if (mLocalOnlyHotspotReservation != null) {
697             Slogf.i(TAG, "Local-only hotspot is already registered.");
698             sendApStarted(mLocalOnlyHotspotReservation.getSoftApConfiguration());
699             return;
700         }
701 
702         Optional<SoftApConfiguration> optionalApConfig =
703                 mStableLocalOnlyHotspotConfig ? restoreApConfiguration() : Optional.empty();
704 
705         if (!optionalApConfig.isPresent()) {
706             Slogf.i(TAG, "Requesting to start local-only hotspot.");
707             mWifiManager.startLocalOnlyHotspot(new ProjectionLocalOnlyHotspotCallback(), mHandler);
708         } else {
709             Slogf.i(TAG, "Requesting to start local-only hotspot with stable configuration.");
710             mWifiManager.startLocalOnlyHotspot(
711                     optionalApConfig.get(),
712                     new HandlerExecutor(mHandler),
713                     new ProjectionLocalOnlyHotspotCallback());
714         }
715     }
716 
getSharedPreferences()717     private SharedPreferences getSharedPreferences() {
718         return mContext.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
719     }
720 
persistApConfiguration(final SoftApConfiguration apConfig)721     private void persistApConfiguration(final SoftApConfiguration apConfig) {
722         synchronized (mLock) {
723             if (apConfig.equals(mApConfiguration)) {
724                 return;  // Configuration didn't change - nothing to store.
725             }
726             mApConfiguration = apConfig;
727         }
728 
729         getSharedPreferences()
730                 .edit()
731                 .putString(KEY_AP_CONFIG_SSID, apConfig.getSsid())
732                 .putString(KEY_AP_CONFIG_BSSID, macAddressToString(apConfig.getBssid()))
733                 .putString(KEY_AP_CONFIG_PASSPHRASE, apConfig.getPassphrase())
734                 .putInt(KEY_AP_CONFIG_SECURITY_TYPE, apConfig.getSecurityType())
735                 .apply();
736         Slogf.i(TAG, "Access Point configuration saved.");
737     }
738 
739     @VisibleForTesting
restoreApConfiguration()740     Optional<SoftApConfiguration> restoreApConfiguration() {
741         synchronized (mLock) {
742             if (mApConfiguration != null) {
743                 return Optional.of(mApConfiguration);
744             }
745         }
746 
747         final SharedPreferences pref = getSharedPreferences();
748         if (pref == null
749                 || !pref.contains(KEY_AP_CONFIG_SSID)
750                 || !pref.contains(KEY_AP_CONFIG_BSSID)
751                 || !pref.contains(KEY_AP_CONFIG_PASSPHRASE)
752                 || !pref.contains(KEY_AP_CONFIG_SECURITY_TYPE)) {
753             Slogf.i(TAG, "AP configuration doesn't exist.");
754             return Optional.empty();
755         }
756 
757         SoftApConfiguration apConfig = new SoftApConfiguration.Builder()
758                 .setSsid(pref.getString(KEY_AP_CONFIG_SSID, ""))
759                 .setBssid(MacAddress.fromString(pref.getString(KEY_AP_CONFIG_BSSID, "")))
760                 .setPassphrase(
761                         pref.getString(KEY_AP_CONFIG_PASSPHRASE, ""),
762                         pref.getInt(KEY_AP_CONFIG_SECURITY_TYPE, 0))
763                 .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE)
764                 .build();
765 
766         synchronized (mLock) {
767             mApConfiguration = apConfig;
768         }
769         return Optional.of(apConfig);
770     }
771 
772     @GuardedBy("mLock")
stopLocalOnlyApLocked()773     private void stopLocalOnlyApLocked() {
774         Slogf.i(TAG, "stopLocalOnlyApLocked");
775 
776         if (mLocalOnlyHotspotReservation == null) {
777             Slogf.w(TAG, "Requested to stop local-only hotspot which was already stopped.");
778             return;
779         }
780 
781         mLocalOnlyHotspotReservation.close();
782         mLocalOnlyHotspotReservation = null;
783     }
784 
sendApStarted(SoftApConfiguration softApConfiguration)785     private void sendApStarted(SoftApConfiguration softApConfiguration) {
786         Message message = Message.obtain();
787         message.what = CarProjectionManager.PROJECTION_AP_STARTED;
788         message.obj = softApConfiguration;
789         Slogf.i(TAG, "Sending PROJECTION_AP_STARTED, ssid: "
790                 + softApConfiguration.getSsid()
791                 + ", apBand: " + softApConfiguration.getBand()
792                 + ", apChannel: " + softApConfiguration.getChannel()
793                 + ", bssid: " + softApConfiguration.getBssid());
794         sendApStatusMessage(message);
795     }
796 
sendApStopped()797     private void sendApStopped() {
798         Message message = Message.obtain();
799         message.what = CarProjectionManager.PROJECTION_AP_STOPPED;
800         sendApStatusMessage(message);
801         unregisterWirelessClients();
802     }
803 
sendApFailed(int reason)804     private void sendApFailed(int reason) {
805         Message message = Message.obtain();
806         message.what = CarProjectionManager.PROJECTION_AP_FAILED;
807         message.arg1 = reason;
808         sendApStatusMessage(message);
809         unregisterWirelessClients();
810     }
811 
sendApStatusMessage(Message message)812     private void sendApStatusMessage(Message message) {
813         List<WirelessClient> clients;
814         synchronized (mLock) {
815             clients = new ArrayList<>(mWirelessClients.values());
816         }
817         for (WirelessClient client : clients) {
818             client.send(message);
819         }
820     }
821 
822     @Override
init()823     public void init() {
824         mContext.registerReceiver(
825                 mBroadcastReceiver, new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION),
826                 Context.RECEIVER_NOT_EXPORTED);
827     }
828 
handleWifiApStateChange(int currState, int prevState, int errorCode, String ifaceName, int mode)829     private void handleWifiApStateChange(int currState, int prevState, int errorCode,
830             String ifaceName, int mode) {
831         if (currState == WIFI_AP_STATE_ENABLING || currState == WIFI_AP_STATE_ENABLED) {
832             Slogf.d(TAG,
833                     "handleWifiApStateChange, curState: " + currState + ", prevState: " + prevState
834                             + ", errorCode: " + errorCode + ", ifaceName: " + ifaceName + ", mode: "
835                             + mode);
836 
837             try {
838                 NetworkInterface iface = NetworkInterface.getByName(ifaceName);
839                 if (iface == null) {
840                     Slogf.e(TAG, "Can't find NetworkInterface: " + ifaceName);
841                 } else {
842                     setAccessPointBssid(MacAddress.fromBytes(iface.getHardwareAddress()));
843                 }
844             } catch (SocketException e) {
845                 Slogf.e(TAG, e.toString(), e);
846             }
847         }
848     }
849 
850     @VisibleForTesting
setAccessPointBssid(MacAddress bssid)851     void setAccessPointBssid(MacAddress bssid) {
852         mApBssid = bssid;
853     }
854 
855     @Override
release()856     public void release() {
857         synchronized (mLock) {
858             mKeyEventHandlers.clear();
859         }
860         mContext.unregisterReceiver(mBroadcastReceiver);
861     }
862 
863     @Override
onBinderDeath( BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler> iface)864     public void onBinderDeath(
865             BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler> iface) {
866         unregisterKeyEventHandler(iface.binderInterface);
867     }
868 
869     @Override
870     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)871     public void dump(IndentingPrintWriter writer) {
872         writer.println("**CarProjectionService**");
873         synchronized (mLock) {
874             writer.println("Registered key event handlers:");
875             for (BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler>
876                     handler : mKeyEventHandlers.getInterfaces()) {
877                 ProjectionKeyEventHandler
878                         projectionKeyEventHandler = (ProjectionKeyEventHandler) handler;
879                 writer.print("  ");
880                 writer.println(projectionKeyEventHandler.toString());
881             }
882 
883             writer.println("Local-only hotspot reservation: " + mLocalOnlyHotspotReservation);
884             writer.println("Stable local-only hotspot configuration: "
885                     + mStableLocalOnlyHotspotConfig);
886             writer.println("Wireless clients: " +  mWirelessClients.size());
887             writer.println("Current wifi mode: " + mWifiMode);
888             writer.println("SoftApCallback: " + mSoftApCallback);
889             writer.println("Bound to projection app: " + mBound);
890             writer.println("Registered Service: " + mRegisteredService);
891             writer.println("Current projection state: " + mCurrentProjectionState);
892             writer.println("Current projection package: " + mCurrentProjectionPackage);
893             writer.println("Projection status: " + mProjectionReceiverClients);
894             writer.println("Projection status listeners: "
895                     + mProjectionStatusListeners.getInterfaces());
896             writer.println("WifiScanner: " + mWifiScanner);
897         }
898     }
899 
900     @Override
onKeyEvent(@arProjectionManager.KeyEventNum int keyEvent)901     public void onKeyEvent(@CarProjectionManager.KeyEventNum int keyEvent) {
902         Slogf.d(TAG, "Dispatching key event: " + keyEvent);
903         synchronized (mLock) {
904             for (BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler>
905                     eventHandlerInterface : mKeyEventHandlers.getInterfaces()) {
906                 ProjectionKeyEventHandler eventHandler =
907                         (ProjectionKeyEventHandler) eventHandlerInterface;
908 
909                 if (eventHandler.canHandleEvent(keyEvent)) {
910                     try {
911                         // oneway
912                         eventHandler.binderInterface.onKeyEvent(keyEvent);
913                     } catch (RemoteException e) {
914                         Slogf.e(TAG, "Cannot dispatch event to client", e);
915                     }
916                 }
917             }
918         }
919     }
920 
921     @GuardedBy("mLock")
updateInputServiceHandlerLocked()922     private void updateInputServiceHandlerLocked() {
923         BitSet newEvents = computeHandledEventsLocked();
924 
925         if (!newEvents.isEmpty()) {
926             mCarInputService.setProjectionKeyEventHandler(this, newEvents);
927         } else {
928             mCarInputService.setProjectionKeyEventHandler(null, null);
929         }
930     }
931 
932     @GuardedBy("mLock")
computeHandledEventsLocked()933     private BitSet computeHandledEventsLocked() {
934         BitSet rv = new BitSet();
935         for (BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler>
936                 handlerInterface : mKeyEventHandlers.getInterfaces()) {
937             rv.or(((ProjectionKeyEventHandler) handlerInterface).mHandledEvents);
938         }
939         return rv;
940     }
941 
setUiMode(Integer uiMode)942     void setUiMode(Integer uiMode) {
943         synchronized (mLock) {
944             mProjectionOptions = createProjectionOptionsBuilder()
945                     .setUiMode(uiMode)
946                     .build();
947         }
948     }
949 
setAccessPointTethering(boolean tetherEnabled)950     void setAccessPointTethering(boolean tetherEnabled) {
951         synchronized (mLock) {
952             mWifiMode = tetherEnabled ? WIFI_MODE_TETHERED : WIFI_MODE_LOCALONLY;
953         }
954     }
955 
setStableLocalOnlyHotspotConfig(boolean stableConfig)956     void setStableLocalOnlyHotspotConfig(boolean stableConfig) {
957         synchronized (mLock) {
958             mStableLocalOnlyHotspotConfig = stableConfig;
959         }
960     }
961 
962     private static class ProjectionKeyEventHandlerContainer
963             extends BinderInterfaceContainer<ICarProjectionKeyEventHandler> {
ProjectionKeyEventHandlerContainer(CarProjectionService service)964         ProjectionKeyEventHandlerContainer(CarProjectionService service) {
965             super(service);
966         }
967 
get(ICarProjectionKeyEventHandler projectionCallback)968         ProjectionKeyEventHandler get(ICarProjectionKeyEventHandler projectionCallback) {
969             return (ProjectionKeyEventHandler) getBinderInterface(projectionCallback);
970         }
971     }
972 
973     private static class ProjectionKeyEventHandler extends
974             BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler> {
975         private BitSet mHandledEvents;
976 
ProjectionKeyEventHandler( ProjectionKeyEventHandlerContainer holder, ICarProjectionKeyEventHandler binder, BitSet handledEvents)977         private ProjectionKeyEventHandler(
978                 ProjectionKeyEventHandlerContainer holder,
979                 ICarProjectionKeyEventHandler binder,
980                 BitSet handledEvents) {
981             super(holder, binder);
982             mHandledEvents = handledEvents;
983         }
984 
canHandleEvent(int event)985         private boolean canHandleEvent(int event) {
986             return mHandledEvents.get(event);
987         }
988 
setHandledEvents(BitSet handledEvents)989         private void setHandledEvents(BitSet handledEvents) {
990             mHandledEvents = handledEvents;
991         }
992 
993         @Override
toString()994         public String toString() {
995             return "ProjectionKeyEventHandler{events=" + mHandledEvents + "}";
996         }
997     }
998 
registerWirelessClient(WirelessClient client)999     private void registerWirelessClient(WirelessClient client) throws RemoteException {
1000         synchronized (mLock) {
1001             if (unregisterWirelessClientLocked(client.token)) {
1002                 Slogf.i(TAG, "Client was already registered, override it.");
1003             }
1004             mWirelessClients.put(client.token, client);
1005         }
1006         client.token.linkToDeath(new WirelessClientDeathRecipient(this, client), 0);
1007     }
1008 
unregisterWirelessClients()1009     private void unregisterWirelessClients() {
1010         synchronized (mLock) {
1011             for (WirelessClient client: mWirelessClients.values()) {
1012                 client.token.unlinkToDeath(client.deathRecipient, 0);
1013             }
1014             mWirelessClients.clear();
1015         }
1016     }
1017 
1018     @GuardedBy("mLock")
unregisterWirelessClientLocked(IBinder token)1019     private boolean unregisterWirelessClientLocked(IBinder token) {
1020         WirelessClient client = mWirelessClients.remove(token);
1021         if (client != null) {
1022             token.unlinkToDeath(client.deathRecipient, 0);
1023         }
1024 
1025         return client != null;
1026     }
1027 
ensureApConfiguration()1028     private void ensureApConfiguration() {
1029         // Always prefer 5GHz configuration whenever it is available.
1030         SoftApConfiguration apConfig = mWifiManager.getSoftApConfiguration();
1031         if (apConfig != null && apConfig.getBand() != SoftApConfiguration.BAND_5GHZ
1032                 && mWifiManager.is5GHzBandSupported()) {
1033             mWifiManager.setSoftApConfiguration(new SoftApConfiguration.Builder(apConfig)
1034                     .setBand(SoftApConfiguration.BAND_5GHZ).build());
1035         }
1036     }
1037 
1038     private class ProjectionSoftApCallback implements WifiManager.SoftApCallback {
1039         private boolean mCurrentStateCall = true;
1040 
1041         @Override
onStateChanged(int state, int softApFailureReason)1042         public void onStateChanged(int state, int softApFailureReason) {
1043             Slogf.i(TAG, "ProjectionSoftApCallback, onStateChanged, state: " + state
1044                     + ", failed reason: " + softApFailureReason
1045                     + ", currentStateCall: " + mCurrentStateCall);
1046             if (mCurrentStateCall) {
1047                 // When callback gets registered framework always sends the current state as the
1048                 // first call. We should ignore current state call to be in par with
1049                 // local-only behavior.
1050                 mCurrentStateCall = false;
1051                 return;
1052             }
1053 
1054             switch (state) {
1055                 case WifiManager.WIFI_AP_STATE_ENABLED: {
1056                     sendApStarted(mWifiManager.getSoftApConfiguration());
1057                     break;
1058                 }
1059                 case WifiManager.WIFI_AP_STATE_DISABLED: {
1060                     sendApStopped();
1061                     break;
1062                 }
1063                 case WifiManager.WIFI_AP_STATE_FAILED: {
1064                     Slogf.w(TAG, "WIFI_AP_STATE_FAILED, reason: " + softApFailureReason);
1065                     int reason;
1066                     switch (softApFailureReason) {
1067                         case WifiManager.SAP_START_FAILURE_NO_CHANNEL:
1068                             reason = ProjectionAccessPointCallback.ERROR_NO_CHANNEL;
1069                             break;
1070                         default:
1071                             reason = ProjectionAccessPointCallback.ERROR_GENERIC;
1072                     }
1073                     sendApFailed(reason);
1074                     break;
1075                 }
1076             }
1077         }
1078 
1079         @Override
onConnectedClientsChanged(List<WifiClient> clients)1080         public void onConnectedClientsChanged(List<WifiClient> clients) {
1081             if (DBG) {
1082                 Slogf.d(TAG, "ProjectionSoftApCallback, onConnectedClientsChanged with "
1083                         + clients.size() + " clients");
1084             }
1085         }
1086     }
1087 
1088     private static class WirelessClient {
1089         public final Messenger messenger;
1090         public final IBinder token;
1091         public @Nullable DeathRecipient deathRecipient;
1092 
WirelessClient(Messenger messenger, IBinder token)1093         private WirelessClient(Messenger messenger, IBinder token) {
1094             this.messenger = messenger;
1095             this.token = token;
1096         }
1097 
of(Messenger messenger, IBinder token)1098         private static WirelessClient of(Messenger messenger, IBinder token) {
1099             return new WirelessClient(messenger, token);
1100         }
1101 
send(Message message)1102         void send(Message message) {
1103             try {
1104                 Slogf.d(TAG, "Sending message " + message.what + " to " + this);
1105                 messenger.send(message);
1106             } catch (RemoteException e) {
1107                 Slogf.e(TAG, "Failed to send message", e);
1108             }
1109         }
1110 
1111         @Override
toString()1112         public String toString() {
1113             return getClass().getSimpleName()
1114                     + "{token= " + token
1115                     + ", deathRecipient=" + deathRecipient + "}";
1116         }
1117     }
1118 
1119     private static class WirelessClientDeathRecipient implements DeathRecipient {
1120         final WeakReference<CarProjectionService> mServiceRef;
1121         final WirelessClient mClient;
1122 
WirelessClientDeathRecipient(CarProjectionService service, WirelessClient client)1123         WirelessClientDeathRecipient(CarProjectionService service, WirelessClient client) {
1124             mServiceRef = new WeakReference<>(service);
1125             mClient = client;
1126             mClient.deathRecipient = this;
1127         }
1128 
1129         @Override
binderDied()1130         public void binderDied() {
1131             Slogf.w(TAG, "Wireless client " + mClient + " died.");
1132             CarProjectionService service = mServiceRef.get();
1133             if (service == null) return;
1134 
1135             synchronized (service.mLock) {
1136                 service.unregisterWirelessClientLocked(mClient.token);
1137             }
1138         }
1139     }
1140 
1141     private static class ProjectionReceiverClient {
1142         private final DeathRecipient mDeathRecipient;
1143         private ProjectionStatus mProjectionStatus;
1144 
ProjectionReceiverClient(DeathRecipient deathRecipient)1145         ProjectionReceiverClient(DeathRecipient deathRecipient) {
1146             mDeathRecipient = deathRecipient;
1147         }
1148 
1149         @Override
toString()1150         public String toString() {
1151             return "ProjectionReceiverClient{"
1152                     + "mDeathRecipient=" + mDeathRecipient
1153                     + ", mProjectionStatus=" + mProjectionStatus
1154                     + '}';
1155         }
1156     }
1157 
macAddressToString(MacAddress macAddress)1158     private static String macAddressToString(MacAddress macAddress) {
1159         byte[] addr = macAddress.toByteArray();
1160         return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
1161                 addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
1162     }
1163 
1164     private class ProjectionLocalOnlyHotspotCallback extends LocalOnlyHotspotCallback {
1165         @Override
onStarted(LocalOnlyHotspotReservation reservation)1166         public void onStarted(LocalOnlyHotspotReservation reservation) {
1167             Slogf.d(TAG, "Local-only hotspot started");
1168             boolean shouldPersistSoftApConfig;
1169             synchronized (mLock) {
1170                 mLocalOnlyHotspotReservation = reservation;
1171                 shouldPersistSoftApConfig = mStableLocalOnlyHotspotConfig;
1172             }
1173             SoftApConfiguration.Builder softApConfigurationBuilder =
1174                     new SoftApConfiguration.Builder(reservation.getSoftApConfiguration())
1175                             .setBssid(mApBssid);
1176 
1177             if (mApBssid != null) {
1178                 softApConfigurationBuilder
1179                         .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE);
1180             }
1181             SoftApConfiguration softApConfiguration = softApConfigurationBuilder.build();
1182 
1183             if (shouldPersistSoftApConfig) {
1184                 persistApConfiguration(softApConfiguration);
1185             }
1186             sendApStarted(softApConfiguration);
1187         }
1188 
1189         @Override
onStopped()1190         public void onStopped() {
1191             Slogf.i(TAG, "Local-only hotspot stopped.");
1192             synchronized (mLock) {
1193                 if (mLocalOnlyHotspotReservation != null) {
1194                     // We must explicitly released old reservation object, otherwise it may
1195                     // unexpectedly stop LOHS later because it overrode finalize() method.
1196                     mLocalOnlyHotspotReservation.close();
1197                 }
1198                 mLocalOnlyHotspotReservation = null;
1199             }
1200             sendApStopped();
1201         }
1202 
1203         @Override
onFailed(int localonlyHostspotFailureReason)1204         public void onFailed(int localonlyHostspotFailureReason) {
1205             Slogf.w(TAG, "Local-only hotspot failed, reason: "
1206                     + localonlyHostspotFailureReason);
1207             synchronized (mLock) {
1208                 mLocalOnlyHotspotReservation = null;
1209             }
1210             int reason;
1211             switch (localonlyHostspotFailureReason) {
1212                 case LocalOnlyHotspotCallback.ERROR_NO_CHANNEL:
1213                     reason = ProjectionAccessPointCallback.ERROR_NO_CHANNEL;
1214                     break;
1215                 case LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED:
1216                     reason = ProjectionAccessPointCallback.ERROR_TETHERING_DISALLOWED;
1217                     break;
1218                 case LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE:
1219                     reason = ProjectionAccessPointCallback.ERROR_INCOMPATIBLE_MODE;
1220                     break;
1221                 default:
1222                     reason = ERROR_GENERIC;
1223 
1224             }
1225             sendApFailed(reason);
1226         }
1227     }
1228 }
1229