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