1 /* 2 * Copyright (C) 2019 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 static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PROFILES_INHIBITED; 20 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothProfile; 24 import android.car.ICarBluetoothUserService; 25 import android.content.Context; 26 import android.os.Binder; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.os.RemoteException; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.util.Slog; 34 35 import com.android.internal.annotations.GuardedBy; 36 37 import java.io.PrintWriter; 38 import java.util.HashSet; 39 import java.util.Objects; 40 import java.util.Set; 41 import java.util.stream.Collectors; 42 43 /** 44 * Manages the inhibiting of Bluetooth profile connections to and from specific devices. 45 */ 46 public class BluetoothProfileInhibitManager { 47 private static final String TAG = CarLog.tagFor(BluetoothProfileInhibitManager.class); 48 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 49 private static final String SETTINGS_DELIMITER = ","; 50 private static final Binder RESTORED_PROFILE_INHIBIT_TOKEN = new Binder(); 51 private static final long RESTORE_BACKOFF_MILLIS = 1000L; 52 53 private final Context mContext; 54 55 // Per-User information 56 private final int mUserId; 57 private final ICarBluetoothUserService mBluetoothUserProxies; 58 59 private final Object mProfileInhibitsLock = new Object(); 60 61 @GuardedBy("mProfileInhibitsLock") 62 private final SetMultimap<BluetoothConnection, InhibitRecord> mProfileInhibits = 63 new SetMultimap<>(); 64 65 @GuardedBy("mProfileInhibitsLock") 66 private final HashSet<InhibitRecord> mRestoredInhibits = new HashSet<>(); 67 68 @GuardedBy("mProfileInhibitsLock") 69 private final HashSet<BluetoothConnection> mAlreadyDisabledProfiles = new HashSet<>(); 70 71 private final Handler mHandler = new Handler( 72 CarServiceUtils.getHandlerThread(CarBluetoothService.THREAD_NAME).getLooper()); 73 /** 74 * BluetoothConnection - encapsulates the information representing a connection to a device on a 75 * given profile. This object is hashable, encodable and decodable. 76 * 77 * Encodes to the following structure: 78 * <device>/<profile> 79 * 80 * Where, 81 * device - the device we're connecting to, can be null 82 * profile - the profile we're connecting on, can be null 83 */ 84 public static class BluetoothConnection { 85 // Examples: 86 // 01:23:45:67:89:AB/9 87 // null/0 88 // null/null 89 private static final String FLATTENED_PATTERN = 90 "^(([0-9A-F]{2}:){5}[0-9A-F]{2}|null)/([0-9]+|null)$"; 91 92 private final BluetoothDevice mBluetoothDevice; 93 private final Integer mBluetoothProfile; 94 BluetoothConnection(Integer profile, BluetoothDevice device)95 public BluetoothConnection(Integer profile, BluetoothDevice device) { 96 mBluetoothProfile = profile; 97 mBluetoothDevice = device; 98 } 99 getDevice()100 public BluetoothDevice getDevice() { 101 return mBluetoothDevice; 102 } 103 getProfile()104 public Integer getProfile() { 105 return mBluetoothProfile; 106 } 107 108 @Override equals(Object other)109 public boolean equals(Object other) { 110 if (this == other) { 111 return true; 112 } 113 if (!(other instanceof BluetoothConnection)) { 114 return false; 115 } 116 BluetoothConnection otherParams = (BluetoothConnection) other; 117 return Objects.equals(mBluetoothDevice, otherParams.mBluetoothDevice) 118 && Objects.equals(mBluetoothProfile, otherParams.mBluetoothProfile); 119 } 120 121 @Override hashCode()122 public int hashCode() { 123 return Objects.hash(mBluetoothDevice, mBluetoothProfile); 124 } 125 126 @Override toString()127 public String toString() { 128 return encode(); 129 } 130 131 /** 132 * Converts these {@link BluetoothConnection} to a parseable string representation. 133 * 134 * @return A parseable string representation of this BluetoothConnection object. 135 */ encode()136 public String encode() { 137 return mBluetoothDevice + "/" + mBluetoothProfile; 138 } 139 140 /** 141 * Creates a {@link BluetoothConnection} from a previous output of {@link #encode()}. 142 * 143 * @param flattenedParams A flattened string representation of a {@link BluetoothConnection} 144 */ decode(String flattenedParams)145 public static BluetoothConnection decode(String flattenedParams) { 146 if (!flattenedParams.matches(FLATTENED_PATTERN)) { 147 throw new IllegalArgumentException("Bad format for flattened BluetoothConnection"); 148 } 149 150 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 151 if (adapter == null) { 152 return new BluetoothConnection(null, null); 153 } 154 155 String[] parts = flattenedParams.split("/"); 156 157 BluetoothDevice device; 158 if (!"null".equals(parts[0])) { 159 device = adapter.getRemoteDevice(parts[0]); 160 } else { 161 device = null; 162 } 163 164 Integer profile; 165 if (!"null".equals(parts[1])) { 166 profile = Integer.valueOf(parts[1]); 167 } else { 168 profile = null; 169 } 170 171 return new BluetoothConnection(profile, device); 172 } 173 } 174 175 private class InhibitRecord implements IBinder.DeathRecipient { 176 private final BluetoothConnection mParams; 177 private final IBinder mToken; 178 179 private boolean mRemoved = false; 180 InhibitRecord(BluetoothConnection params, IBinder token)181 InhibitRecord(BluetoothConnection params, IBinder token) { 182 this.mParams = params; 183 this.mToken = token; 184 } 185 getParams()186 public BluetoothConnection getParams() { 187 return mParams; 188 } 189 getToken()190 public IBinder getToken() { 191 return mToken; 192 } 193 removeSelf()194 public boolean removeSelf() { 195 synchronized (mProfileInhibitsLock) { 196 if (mRemoved) { 197 return true; 198 } 199 200 if (removeInhibitRecord(this)) { 201 mRemoved = true; 202 return true; 203 } else { 204 return false; 205 } 206 } 207 } 208 209 @Override binderDied()210 public void binderDied() { 211 logd("Releasing inhibit request on profile " 212 + Utils.getProfileName(mParams.getProfile()) 213 + " for device " + mParams.getDevice() 214 + ": requesting process died"); 215 removeSelf(); 216 } 217 } 218 219 /** 220 * Creates a new instance of a BluetoothProfileInhibitManager 221 * 222 * @param context - context of calling code 223 * @param userId - ID of user we want to manage inhibits for 224 * @param bluetoothUserProxies - Set of per-user bluetooth proxies for calling into the 225 * bluetooth stack as the current user. 226 * @return A new instance of a BluetoothProfileInhibitManager 227 */ BluetoothProfileInhibitManager(Context context, int userId, ICarBluetoothUserService bluetoothUserProxies)228 public BluetoothProfileInhibitManager(Context context, int userId, 229 ICarBluetoothUserService bluetoothUserProxies) { 230 mContext = context; 231 mUserId = userId; 232 mBluetoothUserProxies = bluetoothUserProxies; 233 } 234 235 /** 236 * Create {@link InhibitRecord}s for all profile inhibits written to {@link Settings.Secure}. 237 */ load()238 private void load() { 239 String savedBluetoothConnection = Settings.Secure.getStringForUser( 240 mContext.getContentResolver(), KEY_BLUETOOTH_PROFILES_INHIBITED, mUserId); 241 242 if (TextUtils.isEmpty(savedBluetoothConnection)) { 243 return; 244 } 245 246 logd("Restoring profile inhibits: " + savedBluetoothConnection); 247 248 for (String paramsStr : savedBluetoothConnection.split(SETTINGS_DELIMITER)) { 249 try { 250 BluetoothConnection params = BluetoothConnection.decode(paramsStr); 251 InhibitRecord record = new InhibitRecord(params, RESTORED_PROFILE_INHIBIT_TOKEN); 252 mProfileInhibits.put(params, record); 253 mRestoredInhibits.add(record); 254 logd("Restored profile inhibits for " + params); 255 } catch (IllegalArgumentException e) { 256 // We won't ever be able to fix a bad parse, so skip it and move on. 257 loge("Bad format for saved profile inhibit: " + paramsStr + ", " + e); 258 } 259 } 260 } 261 262 /** 263 * Dump all currently-active profile inhibits to {@link Settings.Secure}. 264 */ commit()265 private void commit() { 266 Set<BluetoothConnection> inhibitedProfiles = new HashSet<>(mProfileInhibits.keySet()); 267 // Don't write out profiles that were disabled before a request was made, since 268 // restoring those profiles is a no-op. 269 inhibitedProfiles.removeAll(mAlreadyDisabledProfiles); 270 String savedDisconnects = 271 inhibitedProfiles 272 .stream() 273 .map(BluetoothConnection::encode) 274 .collect(Collectors.joining(SETTINGS_DELIMITER)); 275 276 Settings.Secure.putStringForUser( 277 mContext.getContentResolver(), KEY_BLUETOOTH_PROFILES_INHIBITED, 278 savedDisconnects, mUserId); 279 280 logd("Committed key: " + KEY_BLUETOOTH_PROFILES_INHIBITED + ", value: '" 281 + savedDisconnects + "'"); 282 } 283 284 /** 285 * 286 */ start()287 public void start() { 288 load(); 289 removeRestoredProfileInhibits(); 290 } 291 292 /** 293 * 294 */ stop()295 public void stop() { 296 releaseAllInhibitsBeforeUnbind(); 297 } 298 299 /** 300 * Request to disconnect the given profile on the given device, and prevent it from reconnecting 301 * until either the request is released, or the process owning the given token dies. 302 * 303 * @param device The device on which to inhibit a profile. 304 * @param profile The {@link android.bluetooth.BluetoothProfile} to inhibit. 305 * @param token A {@link IBinder} to be used as an identity for the request. If the process 306 * owning the token dies, the request will automatically be released 307 * @return True if the profile was successfully inhibited, false if an error occurred. 308 */ requestProfileInhibit(BluetoothDevice device, int profile, IBinder token)309 boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) { 310 logd("Request profile inhibit: profile " + Utils.getProfileName(profile) 311 + ", device " + device.getAddress()); 312 BluetoothConnection params = new BluetoothConnection(profile, device); 313 InhibitRecord record = new InhibitRecord(params, token); 314 return addInhibitRecord(record); 315 } 316 317 /** 318 * Undo a previous call to {@link #requestProfileInhibit} with the same parameters, 319 * and reconnect the profile if no other requests are active. 320 * 321 * @param device The device on which to release the inhibit request. 322 * @param profile The profile on which to release the inhibit request. 323 * @param token The token provided in the original call to 324 * {@link #requestBluetoothProfileInhibit}. 325 * @return True if the request was released, false if an error occurred. 326 */ releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token)327 boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) { 328 logd("Release profile inhibit: profile " + Utils.getProfileName(profile) 329 + ", device " + device.getAddress()); 330 331 BluetoothConnection params = new BluetoothConnection(profile, device); 332 InhibitRecord record; 333 record = findInhibitRecord(params, token); 334 335 if (record == null) { 336 Slog.e(TAG, "Record not found"); 337 return false; 338 } 339 340 return record.removeSelf(); 341 } 342 343 /** 344 * Add a profile inhibit record, disabling the profile if necessary. 345 */ addInhibitRecord(InhibitRecord record)346 private boolean addInhibitRecord(InhibitRecord record) { 347 synchronized (mProfileInhibitsLock) { 348 BluetoothConnection params = record.getParams(); 349 if (!isProxyAvailable(params.getProfile())) { 350 return false; 351 } 352 353 Set<InhibitRecord> previousRecords = mProfileInhibits.get(params); 354 if (findInhibitRecord(params, record.getToken()) != null) { 355 Slog.e(TAG, "Inhibit request already registered - skipping duplicate"); 356 return false; 357 } 358 359 try { 360 record.getToken().linkToDeath(record, 0); 361 } catch (RemoteException e) { 362 Slog.e(TAG, "Could not link to death on inhibit token (already dead?)", e); 363 return false; 364 } 365 366 boolean isNewlyAdded = previousRecords.isEmpty(); 367 mProfileInhibits.put(params, record); 368 369 if (isNewlyAdded) { 370 try { 371 int priority = 372 mBluetoothUserProxies.getProfilePriority( 373 params.getProfile(), 374 params.getDevice()); 375 if (priority == BluetoothProfile.PRIORITY_OFF) { 376 // This profile was already disabled (and not as the result of an inhibit). 377 // Add it to the already-disabled list, and do nothing else. 378 mAlreadyDisabledProfiles.add(params); 379 380 logd("Profile " + Utils.getProfileName(params.getProfile()) 381 + " already disabled for device " + params.getDevice() 382 + " - suppressing re-enable"); 383 } else { 384 mBluetoothUserProxies.setProfilePriority( 385 params.getProfile(), 386 params.getDevice(), 387 BluetoothProfile.PRIORITY_OFF); 388 mBluetoothUserProxies.bluetoothDisconnectFromProfile( 389 params.getProfile(), 390 params.getDevice()); 391 logd("Disabled profile " 392 + Utils.getProfileName(params.getProfile()) 393 + " for device " + params.getDevice()); 394 } 395 } catch (RemoteException e) { 396 Slog.e(TAG, "Could not disable profile", e); 397 record.getToken().unlinkToDeath(record, 0); 398 mProfileInhibits.remove(params, record); 399 return false; 400 } 401 } 402 403 commit(); 404 return true; 405 } 406 } 407 408 /** 409 * Find the inhibit record, if any, corresponding to the given parameters and token. 410 * 411 * @param params BluetoothConnection parameter pair that could have an inhibit on it 412 * @param token The token provided in the call to {@link #requestBluetoothProfileInhibit}. 413 * @return InhibitRecord for the connection parameters and token if exists, null otherwise. 414 */ findInhibitRecord(BluetoothConnection params, IBinder token)415 private InhibitRecord findInhibitRecord(BluetoothConnection params, IBinder token) { 416 synchronized (mProfileInhibitsLock) { 417 return mProfileInhibits.get(params) 418 .stream() 419 .filter(r -> r.getToken() == token) 420 .findAny() 421 .orElse(null); 422 } 423 } 424 425 /** 426 * Remove a given profile inhibit record, reconnecting if necessary. 427 */ removeInhibitRecord(InhibitRecord record)428 private boolean removeInhibitRecord(InhibitRecord record) { 429 synchronized (mProfileInhibitsLock) { 430 BluetoothConnection params = record.getParams(); 431 if (!isProxyAvailable(params.getProfile())) { 432 return false; 433 } 434 if (!mProfileInhibits.containsEntry(params, record)) { 435 Slog.e(TAG, "Record already removed"); 436 // Removing something a second time vacuously succeeds. 437 return true; 438 } 439 440 // Re-enable profile before unlinking and removing the record, in case of error. 441 // The profile should be re-enabled if this record is the only one left for that 442 // device and profile combination. 443 if (mProfileInhibits.get(params).size() == 1) { 444 if (!restoreProfilePriority(params)) { 445 return false; 446 } 447 } 448 449 record.getToken().unlinkToDeath(record, 0); 450 mProfileInhibits.remove(params, record); 451 452 commit(); 453 return true; 454 } 455 } 456 457 /** 458 * Re-enable and reconnect a given profile for a device. 459 */ restoreProfilePriority(BluetoothConnection params)460 private boolean restoreProfilePriority(BluetoothConnection params) { 461 if (!isProxyAvailable(params.getProfile())) { 462 return false; 463 } 464 465 if (mAlreadyDisabledProfiles.remove(params)) { 466 // The profile does not need any state changes, since it was disabled 467 // before it was inhibited. Leave it disabled. 468 logd("Not restoring profile " 469 + Utils.getProfileName(params.getProfile()) + " for device " 470 + params.getDevice() + " - was manually disabled"); 471 return true; 472 } 473 474 try { 475 mBluetoothUserProxies.setProfilePriority( 476 params.getProfile(), 477 params.getDevice(), 478 BluetoothProfile.PRIORITY_ON); 479 mBluetoothUserProxies.bluetoothConnectToProfile( 480 params.getProfile(), 481 params.getDevice()); 482 logd("Restored profile " + Utils.getProfileName(params.getProfile()) 483 + " for device " + params.getDevice()); 484 return true; 485 } catch (RemoteException e) { 486 loge("Could not enable profile: " + e); 487 return false; 488 } 489 } 490 491 /** 492 * Try once to remove all restored profile inhibits. 493 * 494 * If the CarBluetoothUserService is not yet available, or it hasn't yet bound its profile 495 * proxies, the removal will fail, and will need to be retried later. 496 */ tryRemoveRestoredProfileInhibits()497 private void tryRemoveRestoredProfileInhibits() { 498 HashSet<InhibitRecord> successfullyRemoved = new HashSet<>(); 499 500 for (InhibitRecord record : mRestoredInhibits) { 501 if (removeInhibitRecord(record)) { 502 successfullyRemoved.add(record); 503 } 504 } 505 506 mRestoredInhibits.removeAll(successfullyRemoved); 507 } 508 509 /** 510 * Keep trying to remove all profile inhibits that were restored from settings 511 * until all such inhibits have been removed. 512 */ removeRestoredProfileInhibits()513 private void removeRestoredProfileInhibits() { 514 synchronized (mProfileInhibitsLock) { 515 tryRemoveRestoredProfileInhibits(); 516 517 if (!mRestoredInhibits.isEmpty()) { 518 logd("Could not remove all restored profile inhibits - " 519 + "trying again in " + RESTORE_BACKOFF_MILLIS + "ms"); 520 mHandler.postDelayed( 521 this::removeRestoredProfileInhibits, 522 RESTORED_PROFILE_INHIBIT_TOKEN, 523 RESTORE_BACKOFF_MILLIS); 524 } 525 } 526 } 527 528 /** 529 * Release all active inhibit records prior to user switch or shutdown 530 */ releaseAllInhibitsBeforeUnbind()531 private void releaseAllInhibitsBeforeUnbind() { 532 logd("Unbinding CarBluetoothUserService - releasing all profile inhibits"); 533 534 synchronized (mProfileInhibitsLock) { 535 for (BluetoothConnection params : mProfileInhibits.keySet()) { 536 for (InhibitRecord record : mProfileInhibits.get(params)) { 537 record.removeSelf(); 538 } 539 } 540 541 // Some inhibits might be hanging around because they couldn't be cleaned up. 542 // Make sure they get persisted... 543 commit(); 544 545 // ...then clear them from the map. 546 mProfileInhibits.clear(); 547 548 // We don't need to maintain previously-disabled profiles any more - they were already 549 // skipped in saveProfileInhibitsToSettings() above, and they don't need any 550 // further handling when the user resumes. 551 mAlreadyDisabledProfiles.clear(); 552 553 // Clean up bookkeeping for restored inhibits. (If any are still around, they'll be 554 // restored again when this user restarts.) 555 mHandler.removeCallbacksAndMessages(RESTORED_PROFILE_INHIBIT_TOKEN); 556 mRestoredInhibits.clear(); 557 } 558 } 559 560 /** 561 * Determines if the per-user bluetooth proxy for a given profile is active and usable. 562 * 563 * @return True if proxy is available, false otherwise 564 */ isProxyAvailable(int profile)565 private boolean isProxyAvailable(int profile) { 566 try { 567 return mBluetoothUserProxies.isBluetoothConnectionProxyAvailable(profile); 568 } catch (RemoteException e) { 569 loge("Car BT Service Remote Exception. Proxy for " + Utils.getProfileName(profile) 570 + " not available."); 571 } 572 return false; 573 } 574 575 /** 576 * Print the verbose status of the object 577 */ dump(PrintWriter writer, String indent)578 public void dump(PrintWriter writer, String indent) { 579 writer.println(indent + TAG + ":"); 580 581 // User metadata 582 writer.println(indent + "\tUser: " + mUserId); 583 584 // Current inhibits 585 String inhibits; 586 synchronized (mProfileInhibitsLock) { 587 inhibits = mProfileInhibits.keySet().toString(); 588 } 589 writer.println(indent + "\tInhibited profiles: " + inhibits); 590 } 591 592 /** 593 * Log a message to Log.DEBUG 594 */ logd(String msg)595 private void logd(String msg) { 596 if (DBG) { 597 Slog.d(TAG, "[User: " + mUserId + "] " + msg); 598 } 599 } 600 601 /** 602 * Log a message to Log.WARN 603 */ logw(String msg)604 private void logw(String msg) { 605 Slog.w(TAG, "[User: " + mUserId + "] " + msg); 606 } 607 608 /** 609 * Log a message to Log.ERROR 610 */ loge(String msg)611 private void loge(String msg) { 612 Slog.e(TAG, "[User: " + mUserId + "] " + msg); 613 } 614 } 615