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