• 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 android.annotation.Nullable;
20 import android.app.ActivityManager;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothManager;
24 import android.bluetooth.BluetoothProfile;
25 import android.bluetooth.BluetoothStatusCodes;
26 import android.car.builtin.util.Slogf;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.os.Handler;
32 import android.os.ParcelUuid;
33 import android.os.UserHandle;
34 import android.util.Log;
35 import android.util.SparseArray;
36 
37 import com.android.car.CarLog;
38 import com.android.car.CarServiceUtils;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 
46 /**
47  * BluetoothConnectionRetryManager: manages retry attempts for failed connections.
48  * <p>
49  * {@link FirstConnectionTracker} tracks the first auto-connect immediately following bonding,
50  * distinguished from other connection attempts. It automatically retries failed "first connects"
51  * every {@link sRetryFirstConnectTimeoutMs} milliseconds for a maximum of {@link
52  * MAX_RETRY_ATTEMPTS} attempts. It stops tracking a device if all expected profiles successfully
53  * connect for the first time, or if the device unbonds, or if the Bluetooth stack is torn down.
54  */
55 public final class BluetoothConnectionRetryManager {
56     private static final String TAG = CarLog.tagFor(BluetoothConnectionRetryManager.class);
57     private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);
58 
59     private static final int MAX_RETRY_ATTEMPTS = 3;
60     // NOTE: the value is not "final" - it is modified in the unit tests
61     @VisibleForTesting
62     static int sRetryFirstConnectTimeoutMs = 8000;
63 
64     private final Context mContext;
65     @Nullable
66     private Context mUserContext;
67     private BluetoothAdapter mBluetoothAdapter;
68     private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
69     private final Handler mHandler = new Handler(
70             CarServiceUtils.getHandlerThread(CarBluetoothService.THREAD_NAME).getLooper());
71 
72     private static final int[] MANAGED_PROFILES = BluetoothUtils.getManagedProfilesIds();
73 
74     private final FirstConnectionTracker mFirstConnectionTracker;
75 
76     /**
77      * A BroadcastReceiver for the device we are managing.
78      */
79     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
80         @Override
onReceive(Context context, Intent intent)81         public void onReceive(Context context, Intent intent) {
82             String action = intent.getAction();
83             BluetoothDevice device = null;
84 
85             if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
86                 device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
87                 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
88                         BluetoothDevice.ERROR);
89 
90                 mFirstConnectionTracker.handleDeviceBondStateChange(device, bondState);
91             } else if (BluetoothUtils.isAProfileAction(action)) {
92                 device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
93                 int profile = BluetoothUtils.getProfileFromConnectionAction(action);
94                 int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
95                         BluetoothProfile.STATE_DISCONNECTED);
96                 int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
97                         BluetoothProfile.STATE_DISCONNECTED);
98 
99                 mFirstConnectionTracker.handleProfileConnectionStateChange(device, profile,
100                         toState, fromState);
101             }
102         }
103     }
104 
105     /**
106      * A helper class to track the first auto-connect immediately following bonding, distinguished
107      * from other connection attempts.
108      * <p>
109      * This is done by inferring whether a device failed a connection attempt, and retrying the
110      * connection if the device hasn't connected before since bonding. This does not persist across
111      * Bluetooth sessions, e.g., if a device bonds and Bluetooth is restarted before the device
112      * successfully connects on all supported profiles, {@code FirstConnectionTracker} will
113      * *no longer* track the device as one that yet to connect.
114      */
115     private final class FirstConnectionTracker {
116         /**
117          * A simple counter to track the number of retry attempts a profile has made on a device.
118          * The counter instance also serves as a token to associate enqueued retry attempts to the
119          * (device, profile).
120          * <p>
121          * It does not get decremented or reset to {@code 0}. Connection attempts not initated by
122          * {@code FirstConnectionTracker} do not count, e.g., manual connections by the user, or
123          * possibly other connection retry attempts that are not first-connection-after-pairing.
124          */
125         private final class RetryTokenAndCounter {
126             private int mRetryAttempts = 0;
getCount()127             int getCount() {
128                 return mRetryAttempts;
129             }
increment()130             int increment() {
131                 return ++mRetryAttempts;
132             }
133         }
134 
135         /**
136          * Tracks devices that have bonded but whose profiles have not successfully connected
137          * at least once during the current Bluetooth session. Tracked profiles are the only
138          * ones that may attempt retries if they fail to connect. Note: does not persist across
139          * restarts of Bluetooth.
140          * <p>
141          * Key: A remote device's BD_ADDR. A BD_ADDR is present if and only if it is being
142          * tracked. A device starts getting tracked if it bonds during the current Bluetooth
143          * session. A device is untracked when (1) all supported profiles on the device
144          * successfully connect for the first time; or (2) the device unbonds beforehand.
145          * <p>
146          * Value: A {@link SparseArray} of {@link RetryTokenAndCounter}, a token for each supported
147          * profile. A token is present in the array if and only if its profile is being tracked. A
148          * profile starts getting tracked when the device starts getting tracked (i.e., the device
149          * bonds). A profile is untracked when (1) the profile successfully connects for the first
150          * time; or (2) the device is untracked beforehand (e.g., the device unbonds).
151          */
152         private Map<String, SparseArray<RetryTokenAndCounter>> mBondedYetToConnect =
153                 new HashMap<>();
154 
155         // Only purpose is to help {@link isRetryPosted}.
156         private static final int RETRY_MSG_WHAT = 0;
157 
handleDeviceBondStateChange(BluetoothDevice device, int state)158         void handleDeviceBondStateChange(BluetoothDevice device, int state) {
159             if (DBG) {
160                 Slogf.d(TAG, "Bond state has changed [device: %s, state: %s]", device,
161                         BluetoothUtils.getBondStateName(state));
162             }
163             if ((state == BluetoothDevice.BOND_BONDED) && !isDeviceBeingTracked(device)) {
164                 // an untracked device has bonded
165                 trackDevice(device);
166             } else if ((state == BluetoothDevice.BOND_NONE) && isDeviceBeingTracked(device)) {
167                 // a tracked device has unbonded
168                 untrackDevice(device);
169             }
170         }
171 
handleProfileConnectionStateChange(BluetoothDevice device, int profile, int toState, int fromState)172         void handleProfileConnectionStateChange(BluetoothDevice device, int profile, int toState,
173                 int fromState) {
174             // We are interested in connection state for *tracked* (device, profile)'s only
175             if (!isProfileBeingTracked(device, profile)) {
176                 return;
177             }
178             if (toState == BluetoothProfile.STATE_CONNECTED) {
179                 // tracked (device, profile) has successfully connected
180                 if (DBG) {
181                     Slogf.d(TAG, "%s has connected for the first time on %s.", device,
182                             BluetoothUtils.getProfileName(profile));
183                 }
184                 untrackProfile(device, profile);
185             } else if ((fromState == BluetoothProfile.STATE_CONNECTING)
186                     && ((toState == BluetoothProfile.STATE_DISCONNECTING) || (toState
187                     == BluetoothProfile.STATE_DISCONNECTED))) {
188                 // Proxy for detecting a failed connection, until we get callbacks with
189                 // status codes. Caveats:
190                 // * False positives, e.g., a disconnect attempt during connecting.
191                 // * PAN doesn't seem to follow these state transitions.
192                 if (DBG) {
193                     Slogf.d(TAG, "%s has failed to connect on %s.", device,
194                             BluetoothUtils.getProfileName(profile));
195                 }
196                 RetryTokenAndCounter counter = mBondedYetToConnect.get(device.getAddress())
197                         .get(profile);
198                 if ((counter.getCount() < MAX_RETRY_ATTEMPTS)
199                         && !isRetryPosted(device, profile)) {
200                     // Retry connection attempt.
201                     // Do not post a retry if there is already an outstanding retry posted.
202                     // This ensures retries are posted at least {@link
203                     // sRetryFirstConnectTimeoutMs} apart.
204                     int countForLogs = counter.increment();
205                     mHandler.postDelayed(() -> {
206                         if (DBG) {
207                             Slogf.d(TAG, "[%s, %s] retry attempt (%s/%s)", device,
208                                     BluetoothUtils.getProfileName(profile),
209                                     countForLogs, MAX_RETRY_ATTEMPTS);
210                         }
211                         connect(device);
212                     }, /* token */ counter, sRetryFirstConnectTimeoutMs);
213                     // Only purpose is to help {@link isRetryPosted}.
214                     mHandler.sendMessage(mHandler.obtainMessage(RETRY_MSG_WHAT,
215                             /* token */ counter));
216                 }
217             }
218         }
219 
220         /**
221          * Adds {@code device} to {@link mBondedYetToConnect}.
222          * <p>
223          * Assumes device exists (i.e., {@code device != null}) and is not already tracked (i.e.,
224          * {@code mBondedYetToConnect.containsKey(device.getAddress()) == false}).
225          */
trackDevice(BluetoothDevice device)226         private void trackDevice(BluetoothDevice device) {
227             if (DBG) {
228                 Slogf.d(TAG, "Tracking %s, supported profiles:", device);
229                 // additional debug messages continued in for-loop below
230             }
231             List<ParcelUuid> ourUuids = mBluetoothAdapter.getUuidsList();
232             SparseArray<RetryTokenAndCounter> profileCounters =
233                     new SparseArray<RetryTokenAndCounter>(MANAGED_PROFILES.length);
234             for (int i = 0; i < MANAGED_PROFILES.length; i++) {
235                 int profileId = MANAGED_PROFILES[i];
236                 if (BluetoothUtils.isProfileSupported(ourUuids, device, profileId)) {
237                     if (DBG) {
238                         // debug messaging continued from above
239                         Slogf.d(TAG, "    %s", BluetoothUtils.getProfileName(profileId));
240                     }
241                     profileCounters.put(profileId, new RetryTokenAndCounter());
242                 }
243             }
244             mBondedYetToConnect.put(device.getAddress(), profileCounters);
245         }
246 
247         /**
248          * Removes {@code device} from {@link mBondedYetToConnect}. Also removes any pending retry
249          * attempts.
250          * <p>
251          * Assumes device exists (i.e., {@code device != null}) and is being tracked
252          * (i.e., {@code mBondedYetToConnect.containsKey(device.getAddress()) == true}).
253          */
untrackDevice(BluetoothDevice device)254         private void untrackDevice(BluetoothDevice device) {
255             SparseArray<RetryTokenAndCounter> profileTokens =
256                     mBondedYetToConnect.get(device.getAddress());
257             for (int i = 0; i < profileTokens.size(); i++) {
258                 mHandler.removeCallbacksAndMessages(profileTokens.valueAt(i));
259             }
260             mBondedYetToConnect.remove(device.getAddress());
261         }
262 
untrackProfile(BluetoothDevice device, int profile)263         private void untrackProfile(BluetoothDevice device, int profile) {
264             RetryTokenAndCounter token = mBondedYetToConnect.get(device.getAddress()).get(profile);
265             if (token == null) {
266                 Slogf.w(TAG, "Untracking profile, no token found for %s on device: %s",
267                         BluetoothUtils.getProfileName(profile), device);
268                 return;
269             }
270             mHandler.removeCallbacksAndMessages(token);
271             mBondedYetToConnect.get(device.getAddress()).delete(profile);
272             if (mBondedYetToConnect.get(device.getAddress()).size() == 0) {
273                 untrackDevice(device);
274             }
275         }
276 
277         /**
278          * Returns {@code true} if {@code device} is being tracked, and {@code false} otherwise.
279          * <p>
280          * Assumes {@code device != null}.
281          */
isDeviceBeingTracked(BluetoothDevice device)282         private boolean isDeviceBeingTracked(BluetoothDevice device) {
283             return mBondedYetToConnect.containsKey(device.getAddress());
284         }
285 
isProfileBeingTracked(BluetoothDevice device, int profile)286         private boolean isProfileBeingTracked(BluetoothDevice device, int profile) {
287             SparseArray<RetryTokenAndCounter> profileTokens =
288                     mBondedYetToConnect.get(device.getAddress());
289             if (profileTokens == null) {
290                 return false;
291             }
292             return profileTokens.contains(profile);
293         }
294 
295         /**
296          * Determine if retry attempts have been posted.
297          */
isRetryPosted(BluetoothDevice device, int profile)298         boolean isRetryPosted(BluetoothDevice device, int profile) {
299             if (!isProfileBeingTracked(device, profile)) {
300                 // An untracked (device, profile) should have had any pending callbacks and
301                 // messages removed
302                 if (DBG) {
303                     Slogf.d(TAG, "%s is no longer being tracked on device %s by the time"
304                             + " isRetryPosted was called.",
305                             BluetoothUtils.getProfileName(profile), device);
306                 }
307                 return false;
308             }
309             RetryTokenAndCounter token = mBondedYetToConnect.get(device.getAddress()).get(profile);
310             return mHandler.hasMessages(RETRY_MSG_WHAT, token);
311         }
312     }
313 
314     /**
315      * For unit testing purposes, to aid in verifying retry attempts were posted.
316      */
317     @VisibleForTesting
isRetryPosted(BluetoothDevice device, int profile)318     boolean isRetryPosted(BluetoothDevice device, int profile) {
319         return mFirstConnectionTracker.isRetryPosted(device, profile);
320     }
321 
322     /**
323      * For unit testing purposes.
324      */
325     @VisibleForTesting
getMaxRetriesFirstConnection()326     int getMaxRetriesFirstConnection() {
327         return MAX_RETRY_ATTEMPTS;
328     }
329 
330     /**
331      * Creates an instance of {@link BluetoothConnectionRetryManager} that will manage
332      * connection retries.
333      *
334      * @param context - {@link Context} of calling code.
335      * @return A new instance of a {@link BluetoothConnectionRetryManager}, or {@code null}
336      * on error.
337      */
create(Context context)338     public static BluetoothConnectionRetryManager create(Context context) {
339         try {
340             return new BluetoothConnectionRetryManager(context);
341         } catch (NullPointerException e) {
342             return null;
343         }
344     }
345 
346     /**
347      * Creates an instance of {@link BluetoothConnectionRetryManager} that will manage
348      * connection retries.
349      *
350      * @param context - {@link Context} of calling code.
351      * @return A new instance of a {@link BluetoothConnectionRetryManager}, or {@code null}
352      * on error.
353      */
BluetoothConnectionRetryManager(Context context)354     private BluetoothConnectionRetryManager(Context context) {
355         mContext = Objects.requireNonNull(context);
356         BluetoothManager bluetoothManager =
357                 Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class));
358         mBluetoothAdapter = Objects.requireNonNull(bluetoothManager.getAdapter());
359         mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
360         mFirstConnectionTracker = new FirstConnectionTracker();
361     }
362 
363     /**
364      * Begin managing connection retries.
365      */
init()366     public void init() {
367         if (DBG) {
368             Slogf.d(TAG, "Starting connection retry management, managed profiles:");
369             for (int i = 0; i < MANAGED_PROFILES.length; i++) {
370                 Slogf.d(TAG, "    %s",
371                         BluetoothUtils.getProfileName(MANAGED_PROFILES[i]));
372             }
373         }
374 
375         IntentFilter filter = new IntentFilter();
376         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
377         // TODO (201800664): Profile State Change actions are hidden. This is a work around for now
378         filter.addAction(BluetoothUtils.A2DP_SINK_CONNECTION_STATE_CHANGED);
379         filter.addAction(BluetoothUtils.A2DP_SOURCE_CONNECTION_STATE_CHANGED);
380         filter.addAction(BluetoothUtils.HFP_CLIENT_CONNECTION_STATE_CHANGED);
381         filter.addAction(BluetoothUtils.MAP_CLIENT_CONNECTION_STATE_CHANGED);
382         filter.addAction(BluetoothUtils.PAN_CONNECTION_STATE_CHANGED);
383         filter.addAction(BluetoothUtils.PBAP_CLIENT_CONNECTION_STATE_CHANGED);
384 
385         UserHandle currentUser = UserHandle.of(ActivityManager.getCurrentUser());
386         mUserContext = mContext.createContextAsUser(currentUser, /* flags= */ 0);
387 
388         mUserContext.registerReceiver(mBluetoothBroadcastReceiver, filter);
389     }
390 
391     /**
392      * Stop managing connection retries. Clean up local resources.
393      */
release()394     public void release() {
395         if (DBG) {
396             Slogf.d(TAG, "Stopping connection retry management");
397         }
398 
399         if (mUserContext != null) {
400             if (mBluetoothBroadcastReceiver != null) {
401                 mUserContext.unregisterReceiver(mBluetoothBroadcastReceiver);
402             } else {
403                 Slogf.wtf(TAG, "mBluetoothBroadcastReceiver null during release()");
404             }
405             mUserContext = null;
406         }
407     }
408 
409     /**
410      * Connect a device.
411      *
412      * @param device - The device to connect
413      * @return
414      */
connect(BluetoothDevice device)415     private int connect(BluetoothDevice device) {
416         if (DBG) {
417             Slogf.d(TAG, "Connecting %s", device);
418         }
419         if (device == null) {
420             return BluetoothStatusCodes.ERROR_UNKNOWN;
421         }
422         return device.connect();
423     }
424 }
425