blob: 691592f234e1d882e9d858b5de331510ac391962 [file] [log] [blame]
Sal Savage703c46f2019-04-15 08:39:25 -07001/*
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
17package com.android.car;
18
19import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PROFILES_INHIBITED;
20
21import android.bluetooth.BluetoothAdapter;
22import android.bluetooth.BluetoothDevice;
23import android.bluetooth.BluetoothProfile;
24import android.car.ICarBluetoothUserService;
25import android.content.Context;
26import android.os.Binder;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.Looper;
30import android.os.RemoteException;
31import android.provider.Settings;
32import android.text.TextUtils;
33import android.util.Log;
34
35import com.android.internal.annotations.GuardedBy;
36
37import java.io.PrintWriter;
38import java.util.HashSet;
39import java.util.Objects;
40import java.util.Set;
41import java.util.stream.Collectors;
42
43/**
44 * Manages the inhibiting of Bluetooth profile connections to and from specific devices.
45 */
46public class BluetoothProfileInhibitManager {
47 private static final String TAG = "BluetoothProfileInhibitManager";
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
Sal Savage13921c32019-11-14 15:30:18 -080059 private final Object mProfileInhibitsLock = new Object();
60
61 @GuardedBy("mProfileInhibitsLock")
Sal Savage703c46f2019-04-15 08:39:25 -070062 private final SetMultimap<BluetoothConnection, InhibitRecord> mProfileInhibits =
63 new SetMultimap<>();
64
Sal Savage13921c32019-11-14 15:30:18 -080065 @GuardedBy("mProfileInhibitsLock")
Sal Savage703c46f2019-04-15 08:39:25 -070066 private final HashSet<InhibitRecord> mRestoredInhibits = new HashSet<>();
67
Sal Savage13921c32019-11-14 15:30:18 -080068 @GuardedBy("mProfileInhibitsLock")
Sal Savage703c46f2019-04-15 08:39:25 -070069 private final HashSet<BluetoothConnection> mAlreadyDisabledProfiles = new HashSet<>();
70
71 private final Handler mHandler = new Handler(Looper.getMainLooper());
72
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
95 public BluetoothConnection(Integer profile, BluetoothDevice device) {
96 mBluetoothProfile = profile;
97 mBluetoothDevice = device;
98 }
99
100 public BluetoothDevice getDevice() {
101 return mBluetoothDevice;
102 }
103
104 public Integer getProfile() {
105 return mBluetoothProfile;
106 }
107
108 @Override
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
122 public int hashCode() {
123 return Objects.hash(mBluetoothDevice, mBluetoothProfile);
124 }
125
126 @Override
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 */
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 */
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
181 InhibitRecord(BluetoothConnection params, IBinder token) {
182 this.mParams = params;
183 this.mToken = token;
184 }
185
186 public BluetoothConnection getParams() {
187 return mParams;
188 }
189
190 public IBinder getToken() {
191 return mToken;
192 }
193
194 public boolean removeSelf() {
Sal Savage13921c32019-11-14 15:30:18 -0800195 synchronized (mProfileInhibitsLock) {
Sal Savage703c46f2019-04-15 08:39:25 -0700196 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
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 */
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 */
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 */
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 */
287 public void start() {
288 load();
289 removeRestoredProfileInhibits();
290 }
291
292 /**
293 *
294 */
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 */
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 */
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;
Sal Savage13921c32019-11-14 15:30:18 -0800333 record = findInhibitRecord(params, token);
Sal Savage703c46f2019-04-15 08:39:25 -0700334
335 if (record == null) {
336 Log.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 */
Sal Savage13921c32019-11-14 15:30:18 -0800346 private boolean addInhibitRecord(InhibitRecord record) {
347 synchronized (mProfileInhibitsLock) {
348 BluetoothConnection params = record.getParams();
349 if (!isProxyAvailable(params.getProfile())) {
Sal Savage703c46f2019-04-15 08:39:25 -0700350 return false;
351 }
Sal Savage703c46f2019-04-15 08:39:25 -0700352
Sal Savage13921c32019-11-14 15:30:18 -0800353 Set<InhibitRecord> previousRecords = mProfileInhibits.get(params);
354 if (findInhibitRecord(params, record.getToken()) != null) {
355 Log.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 Log.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 Log.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 }
Sal Savage703c46f2019-04-15 08:39:25 -0700406 }
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 */
415 private InhibitRecord findInhibitRecord(BluetoothConnection params, IBinder token) {
Sal Savage13921c32019-11-14 15:30:18 -0800416 synchronized (mProfileInhibitsLock) {
417 return mProfileInhibits.get(params)
418 .stream()
419 .filter(r -> r.getToken() == token)
420 .findAny()
421 .orElse(null);
422 }
Sal Savage703c46f2019-04-15 08:39:25 -0700423 }
424
425 /**
426 * Remove a given profile inhibit record, reconnecting if necessary.
427 */
Sal Savage13921c32019-11-14 15:30:18 -0800428 private boolean removeInhibitRecord(InhibitRecord record) {
429 synchronized (mProfileInhibitsLock) {
430 BluetoothConnection params = record.getParams();
431 if (!isProxyAvailable(params.getProfile())) {
Sal Savage703c46f2019-04-15 08:39:25 -0700432 return false;
433 }
Sal Savage13921c32019-11-14 15:30:18 -0800434 if (!mProfileInhibits.containsEntry(params, record)) {
435 Log.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;
Sal Savage703c46f2019-04-15 08:39:25 -0700454 }
Sal Savage703c46f2019-04-15 08:39:25 -0700455 }
456
457 /**
458 * Re-enable and reconnect a given profile for a device.
459 */
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 */
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 */
Sal Savage13921c32019-11-14 15:30:18 -0800513 private void removeRestoredProfileInhibits() {
514 synchronized (mProfileInhibitsLock) {
515 tryRemoveRestoredProfileInhibits();
Sal Savage703c46f2019-04-15 08:39:25 -0700516
Sal Savage13921c32019-11-14 15:30:18 -0800517 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 }
Sal Savage703c46f2019-04-15 08:39:25 -0700525 }
526 }
527
528 /**
529 * Release all active inhibit records prior to user switch or shutdown
530 */
Sal Savage13921c32019-11-14 15:30:18 -0800531 private void releaseAllInhibitsBeforeUnbind() {
Sal Savage703c46f2019-04-15 08:39:25 -0700532 logd("Unbinding CarBluetoothUserService - releasing all profile inhibits");
Sal Savage13921c32019-11-14 15:30:18 -0800533
534 synchronized (mProfileInhibitsLock) {
535 for (BluetoothConnection params : mProfileInhibits.keySet()) {
536 for (InhibitRecord record : mProfileInhibits.get(params)) {
537 record.removeSelf();
538 }
Sal Savage703c46f2019-04-15 08:39:25 -0700539 }
Sal Savage13921c32019-11-14 15:30:18 -0800540
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();
Sal Savage703c46f2019-04-15 08:39:25 -0700557 }
Sal Savage703c46f2019-04-15 08:39:25 -0700558 }
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 */
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 */
Sal Savage13921c32019-11-14 15:30:18 -0800578 public void dump(PrintWriter writer, String indent) {
Sal Savage703c46f2019-04-15 08:39:25 -0700579 writer.println(indent + TAG + ":");
580
581 // User metadata
582 writer.println(indent + "\tUser: " + mUserId);
583
584 // Current inhibits
585 String inhibits;
Sal Savage13921c32019-11-14 15:30:18 -0800586 synchronized (mProfileInhibitsLock) {
Sal Savage703c46f2019-04-15 08:39:25 -0700587 inhibits = mProfileInhibits.keySet().toString();
588 }
589 writer.println(indent + "\tInhibited profiles: " + inhibits);
590 }
591
592 /**
593 * Log a message to Log.DEBUG
594 */
595 private void logd(String msg) {
596 if (DBG) {
597 Log.d(TAG, "[User: " + mUserId + "] " + msg);
598 }
599 }
600
601 /**
602 * Log a message to Log.WARN
603 */
604 private void logw(String msg) {
605 Log.w(TAG, "[User: " + mUserId + "] " + msg);
606 }
607
608 /**
609 * Log a message to Log.ERROR
610 */
611 private void loge(String msg) {
612 Log.e(TAG, "[User: " + mUserId + "] " + msg);
613 }
614}