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