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