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