• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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