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