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