1 /*
2  * Copyright (C) 2017 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 com.android.car;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.car.VehicleAreaSeat;
22 import android.car.VehicleAreaType;
23 import android.car.VehiclePropertyIds;
24 import android.car.VehicleSeatOccupancyState;
25 import android.car.drivingstate.CarDrivingStateEvent;
26 import android.car.hardware.CarPropertyConfig;
27 import android.car.hardware.CarPropertyValue;
28 import android.car.hardware.power.CarPowerPolicy;
29 import android.car.hardware.power.CarPowerPolicyFilter;
30 import android.car.hardware.power.ICarPowerPolicyListener;
31 import android.car.hardware.power.PowerComponent;
32 import android.car.hardware.property.CarPropertyEvent;
33 import android.car.hardware.property.CarPropertyManager;
34 import android.car.hardware.property.ICarPropertyEventListener;
35 import android.content.BroadcastReceiver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.os.RemoteException;
40 import android.os.UserHandle;
41 import android.os.UserManager;
42 import android.provider.Settings;
43 import android.util.Log;
44 import android.util.Slog;
45 
46 import com.android.car.power.CarPowerManagementService;
47 import com.android.internal.annotations.VisibleForTesting;
48 
49 import java.io.PrintWriter;
50 import java.util.List;
51 import java.util.Objects;
52 
53 /**
54  * A Bluetooth Device Connection policy that is specific to the use cases of a Car. Contains policy
55  * for deciding when to trigger connection and disconnection events.
56  */
57 public class BluetoothDeviceConnectionPolicy {
58     private static final String TAG = CarLog.tagFor(BluetoothDeviceConnectionPolicy.class);
59     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
60 
61     private final int mUserId;
62     private final Context mContext;
63     private final BluetoothAdapter mBluetoothAdapter;
64     private final CarBluetoothService mCarBluetoothService;
65     private final CarServicesHelper mCarHelper;
66     private final UserManager mUserManager;
67 
68     private final ICarPowerPolicyListener mPowerPolicyListener =
69             new ICarPowerPolicyListener.Stub() {
70                 @Override
71                 public void onPolicyChanged(CarPowerPolicy appliedPolicy,
72                         CarPowerPolicy accumulatedPolicy) {
73                     boolean isOn = accumulatedPolicy.isComponentEnabled(PowerComponent.BLUETOOTH);
74                     if (!mUserManager.isUserUnlocked(mUserId)) {
75                         logd("User " + mUserId + " is locked, ignoring bluetooth power change "
76                                 + (isOn ? "on" : "off"));
77                         return;
78                     }
79                     if (isOn) {
80                         if (isBluetoothPersistedOn()) {
81                             enableBluetooth();
82                         }
83                         // The above isBluetoothPersistedOn() call is always true when the
84                         // adapter is on, but can be true or false if the adapter is off. If we
85                         // turned the adapter back on then this connectDevices() call would fail
86                         // at first here but be caught by the following adapter on broadcast
87                         // below. We'll only do this if the adapter is on.
88                         if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
89                             connectDevices();
90                         }
91                     } else {
92                         // we'll turn off Bluetooth to disconnect devices and better the "off"
93                         // illusion
94                         logd("Car power policy turns off bluetooth. Disable bluetooth adapter");
95                         disableBluetooth();
96                     }
97                 }
98     };
99 
100     @VisibleForTesting
getPowerPolicyListener()101     public ICarPowerPolicyListener getPowerPolicyListener() {
102         return mPowerPolicyListener;
103     }
104 
105     /**
106      * A BroadcastReceiver that listens specifically for actions related to the profile we're
107      * tracking and uses them to update the status.
108      *
109      * On BluetoothAdapter.ACTION_STATE_CHANGED:
110      *    If the adapter is going into the ON state, then commit trigger auto connection.
111      */
112     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
113         @Override
onReceive(Context context, Intent intent)114         public void onReceive(Context context, Intent intent) {
115             String action = intent.getAction();
116             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
117             if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
118                 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
119                 logd("Bluetooth Adapter state changed: ", Utils.getAdapterStateName(state));
120                 if (state == BluetoothAdapter.STATE_ON) {
121                     connectDevices();
122                 }
123             }
124         }
125     }
126     private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
127 
128     /**
129      * A helper class to interact with the VHAL and the rest of the car.
130      */
131     final class CarServicesHelper {
132         private final CarPropertyService mCarPropertyService;
133         private final CarDrivingStateService mCarDrivingStateService;
134 
135         // Location of the driver's seat, e.g., left or right side.
136         private final int mDriverSeat;
137 
CarServicesHelper()138         CarServicesHelper() {
139             mCarPropertyService = CarLocalServices.getService(CarPropertyService.class);
140             if (mCarPropertyService == null) Slog.w(TAG, "Cannot find CarPropertyService");
141             mDriverSeat = getDriverSeatLocationFromVhal();
142             mCarDrivingStateService = CarLocalServices.getService(CarDrivingStateService.class);
143             if (mCarDrivingStateService == null) Slog.w(TAG, "Cannot find mCarDrivingStateService");
144         }
145 
146         /**
147          * Set up vehicle event listeners. Remember to call {@link release()} when done.
148          */
init()149         public void init() {
150             if (mCarPropertyService != null) {
151                 mCarPropertyService.registerListener(VehiclePropertyIds.SEAT_OCCUPANCY,
152                         CarPropertyManager.SENSOR_RATE_ONCHANGE, mSeatOnOccupiedListener);
153             }
154         }
155 
release()156         public void release() {
157             if (mCarPropertyService != null) {
158                 mCarPropertyService.unregisterListener(VehiclePropertyIds.SEAT_OCCUPANCY,
159                         mSeatOnOccupiedListener);
160             }
161         }
162 
163         /**
164          * A {@code ICarPropertyEventListener} that triggers the auto-connection process when
165          * {@code SEAT_OCCUPANCY} is {@code OCCUPIED}.
166          */
167         private final ICarPropertyEventListener mSeatOnOccupiedListener =
168                 new ICarPropertyEventListener.Stub() {
169                     @Override
170                     public void onEvent(List<CarPropertyEvent> events) throws RemoteException {
171                         for (CarPropertyEvent event : events) {
172                             onSeatOccupancyCarPropertyEvent(event);
173                         }
174                     }
175                 };
176 
177         /**
178          * Acts on {@link CarPropertyEvent} events marked with
179          * {@link CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE} and marked with {@link
180          * VehiclePropertyIds.SEAT_OCCUPANCY} by calling {@link connectDevices}.
181          * <p>
182          * Default implementation filters on driver's seat only, but can change to trigger on
183          * any front row seat, or any seat in the car.
184          * <p>
185          * Default implementation also restricts this trigger to when the car is in the
186          * parked state, to discourage drivers from exploiting to connect while driving, and to
187          * also filter out spurious seat sensor signals while driving.
188          * <p>
189          * This method does nothing if the event parameter is {@code null}.
190          *
191          * @param event - The {@link CarPropertyEvent} to be handled.
192          */
onSeatOccupancyCarPropertyEvent(CarPropertyEvent event)193         private void onSeatOccupancyCarPropertyEvent(CarPropertyEvent event) {
194             if ((event == null)
195                     || (event.getEventType() != CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE)) {
196                 return;
197             }
198             CarPropertyValue value = event.getCarPropertyValue();
199             logd("Car property changed: ", value.toString());
200             if (mBluetoothAdapter.isEnabled()
201                     && (value.getPropertyId() == VehiclePropertyIds.SEAT_OCCUPANCY)
202                     && ((int) value.getValue() == VehicleSeatOccupancyState.OCCUPIED)
203                     && (value.getAreaId() == mDriverSeat)
204                     && isParked()) {
205                 connectDevices();
206             }
207         }
208 
209         /**
210          * Gets the location of the driver's seat (e.g., front-left, front-right) from the VHAL.
211          * <p>
212          * Default implementation sets the driver's seat to front-left if mCarPropertyService is
213          * not found.
214          * <p>
215          * Note, comments for {@link CarPropertyManager#getIntProperty(int, int)} indicate it may
216          * take a couple of seconds to complete, whereas there are no such comments for
217          * {@link CarPropertyService#getPropertySafe(int, int)}, but we assume there is also similar
218          * latency in querying VHAL properties.
219          *
220          * @return An {@code int} representing driver's seat location.
221          */
getDriverSeatLocationFromVhal()222         private int getDriverSeatLocationFromVhal() {
223             int defaultLocation = VehicleAreaSeat.SEAT_ROW_1_LEFT;
224 
225             if (mCarPropertyService == null) {
226                 return defaultLocation;
227             }
228             CarPropertyValue value = mCarPropertyService.getPropertySafe(
229                     VehiclePropertyIds.INFO_DRIVER_SEAT, VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL);
230             if (value == null) {
231                 // Distinguish between two possible causes for null, based on
232                 // {@code mConfigs.get(prop)} in {@link CarPropertyService#getProperty} and
233                 // {@link CarPropertyService#getPropertyConfigList}
234                 List<CarPropertyConfig> availableProp = mCarPropertyService.getPropertyConfigList(
235                         new int[] {VehiclePropertyIds.INFO_DRIVER_SEAT});
236                 if (availableProp.isEmpty() || availableProp.get(0) == null) {
237                     logd("Driver seat location property is not in config list.");
238                 } else {
239                     logd("Driver seat location property is not ready yet.");
240                 }
241                 return defaultLocation;
242             }
243             return (int) value.getValue();
244         }
245 
getDriverSeatLocation()246         public int getDriverSeatLocation() {
247             return mDriverSeat;
248         }
249 
250         /**
251          * Returns {@code true} if the car is in parked gear.
252          * <p>
253          * We are being conservative and only want to trigger when car is in parked state. Extending
254          * this conservative approach, we default return false if {@code mCarDrivingStateService}
255          * is not found, or if we otherwise can't get the value.
256          */
isParked()257         public boolean isParked() {
258             if (mCarDrivingStateService == null) {
259                 return false;
260             }
261             CarDrivingStateEvent event = mCarDrivingStateService.getCurrentDrivingState();
262             if (event == null) {
263                 return false;
264             }
265             return event.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED;
266         }
267     }
268 
269     /**
270      * Create a new BluetoothDeviceConnectionPolicy object, responsible for encapsulating the
271      * default policy for when to initiate device connections given the list of prioritized devices
272      * for each profile.
273      *
274      * @param context - The context of the creating application
275      * @param userId - The user ID we're operating as
276      * @param bluetoothService - A reference to CarBluetoothService so we can connect devices
277      * @return A new instance of a BluetoothProfileDeviceManager, or null on any error
278      */
create(Context context, int userId, CarBluetoothService bluetoothService)279     public static BluetoothDeviceConnectionPolicy create(Context context, int userId,
280             CarBluetoothService bluetoothService) {
281         try {
282             return new BluetoothDeviceConnectionPolicy(context, userId, bluetoothService);
283         } catch (NullPointerException e) {
284             return null;
285         }
286     }
287 
288     /**
289      * Create a new BluetoothDeviceConnectionPolicy object, responsible for encapsulating the
290      * default policy for when to initiate device connections given the list of prioritized devices
291      * for each profile.
292      *
293      * @param context - The context of the creating application
294      * @param userId - The user ID we're operating as
295      * @param bluetoothService - A reference to CarBluetoothService so we can connect devices
296      * @return A new instance of a BluetoothProfileDeviceManager
297      */
BluetoothDeviceConnectionPolicy(Context context, int userId, CarBluetoothService bluetoothService)298     private BluetoothDeviceConnectionPolicy(Context context, int userId,
299             CarBluetoothService bluetoothService) {
300         mUserId = userId;
301         mContext = Objects.requireNonNull(context);
302         mCarBluetoothService = bluetoothService;
303         mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
304         mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
305         mCarHelper = new CarServicesHelper();
306         mUserManager = mContext.getSystemService(UserManager.class);
307     }
308 
309     /**
310      * Setup the Bluetooth profile service connections and Vehicle Event listeners.
311      * and start the state machine -{@link BluetoothAutoConnectStateMachine}
312      */
init()313     public void init() {
314         logd("init()");
315         IntentFilter profileFilter = new IntentFilter();
316         profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
317         mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT,
318                 profileFilter, null, null);
319         CarPowerManagementService cpms = CarLocalServices.getService(
320                 CarPowerManagementService.class);
321         if (cpms != null) {
322             CarPowerPolicyFilter filter = new CarPowerPolicyFilter.Builder()
323                     .setComponents(PowerComponent.BLUETOOTH).build();
324             cpms.addPowerPolicyListener(filter, mPowerPolicyListener);
325         } else {
326             Slog.w(TAG, "Cannot find CarPowerManagementService");
327         }
328         mCarHelper.init();
329 
330         // Since we do this only on start up and on user switch, it's safe to kick off a connect on
331         // init. If we have a connect in progress, this won't hurt anything. If we already have
332         // devices connected, this will add on top of it. We _could_ enter this from a crash
333         // recovery, but that would at worst cause more devices to connect and wouldn't change the
334         // existing devices.
335         if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
336             // CarPowerManager doesn't provide a getState() or that would go here too.
337             connectDevices();
338         }
339     }
340 
341     /**
342      * Clean up slate. Close the Bluetooth profile service connections and quit the state machine -
343      * {@link BluetoothAutoConnectStateMachine}
344      */
release()345     public void release() {
346         logd("release()");
347         CarPowerManagementService cpms =
348                 CarLocalServices.getService(CarPowerManagementService.class);
349         if (cpms != null) {
350             cpms.removePowerPolicyListener(mPowerPolicyListener);
351         }
352         if (mBluetoothBroadcastReceiver != null) {
353             mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
354         }
355         mCarHelper.release();
356     }
357 
358     /**
359      * Tell each Profile device manager that its time to begin auto connecting devices
360      */
connectDevices()361     public void connectDevices() {
362         logd("Connect devices for each profile");
363         mCarBluetoothService.connectDevices();
364     }
365 
366     /**
367      * Get the persisted Bluetooth state from Settings
368      *
369      * @return True if the persisted Bluetooth state is on, false otherwise
370      */
isBluetoothPersistedOn()371     private boolean isBluetoothPersistedOn() {
372         return (Settings.Global.getInt(
373                 mContext.getContentResolver(), Settings.Global.BLUETOOTH_ON, -1) != 0);
374     }
375 
376     /**
377      * Turn on the Bluetooth Adapter.
378      */
enableBluetooth()379     private void enableBluetooth() {
380         logd("Enable bluetooth adapter");
381         if (mBluetoothAdapter == null) {
382             Slog.e(TAG, "Cannot enable Bluetooth adapter. The object is null.");
383             return;
384         }
385         mBluetoothAdapter.enable();
386     }
387 
388     /**
389      * Turn off the Bluetooth Adapter.
390      *
391      * Tells BluetoothAdapter to shut down _without_ persisting the off state as the desired state
392      * of the Bluetooth adapter for next start up.
393      */
disableBluetooth()394     private void disableBluetooth() {
395         logd("Disable bluetooth, do not persist state across reboot");
396         if (mBluetoothAdapter == null) {
397             Slog.e(TAG, "Cannot disable Bluetooth adapter. The object is null.");
398             return;
399         }
400         mBluetoothAdapter.disable(false);
401     }
402 
403     /**
404      * Print the verbose status of the object
405      */
dump(PrintWriter writer, String indent)406     public void dump(PrintWriter writer, String indent) {
407         writer.println(indent + TAG + ":");
408         writer.println(indent + "\tUserId: " + mUserId);
409     }
410 
411     /**
412      * Print to debug if debug is enabled
413      */
logd(String... msgParts)414     private static void logd(String... msgParts) {
415         if (DBG) {
416             Slog.d(TAG, String.join(" ", msgParts));
417         }
418     }
419 }
420