• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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.server.nearby.common.bluetooth.fastpair;
18 
19 import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
20 import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
21 import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
22 
23 import static com.google.common.base.Preconditions.checkNotNull;
24 
25 import android.content.Context;
26 import android.os.SystemClock;
27 import android.util.Log;
28 
29 import androidx.annotation.Nullable;
30 import androidx.annotation.VisibleForTesting;
31 import androidx.core.util.Consumer;
32 
33 import com.android.server.nearby.common.bluetooth.BluetoothException;
34 import com.android.server.nearby.common.bluetooth.BluetoothGattException;
35 import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
36 import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
37 import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
38 import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper;
39 import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
40 import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
41 import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
42 import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
43 import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
44 
45 import java.util.concurrent.ExecutionException;
46 import java.util.concurrent.TimeUnit;
47 import java.util.concurrent.TimeoutException;
48 
49 /**
50  * Manager for working with Gatt connections.
51  *
52  * <p>This helper class allows for opening and closing GATT connections to a provided address.
53  * Optionally, it can also support automatically reopening a connection in the case that it has been
54  * closed when it's next needed through {@link Preferences#getAutomaticallyReconnectGattWhenNeeded}.
55  */
56 // TODO(b/202524672): Add class unit test.
57 final class GattConnectionManager {
58 
59     private static final String TAG = GattConnectionManager.class.getSimpleName();
60 
61     private final Context mContext;
62     private final Preferences mPreferences;
63     private final EventLoggerWrapper mEventLogger;
64     private final BluetoothAdapter mBluetoothAdapter;
65     private final ToggleBluetoothTask mToggleBluetooth;
66     private final String mAddress;
67     private final TimingLogger mTimingLogger;
68     private final boolean mSetMtu;
69     @Nullable
70     private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
71     @Nullable
72     private BluetoothGattConnection mGattConnection;
73     private static boolean sTestMode = false;
74 
enableTestMode()75     static void enableTestMode() {
76         sTestMode = true;
77     }
78 
GattConnectionManager( Context context, Preferences preferences, EventLoggerWrapper eventLogger, BluetoothAdapter bluetoothAdapter, ToggleBluetoothTask toggleBluetooth, String address, TimingLogger timingLogger, @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker, boolean setMtu)79     GattConnectionManager(
80             Context context,
81             Preferences preferences,
82             EventLoggerWrapper eventLogger,
83             BluetoothAdapter bluetoothAdapter,
84             ToggleBluetoothTask toggleBluetooth,
85             String address,
86             TimingLogger timingLogger,
87             @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker,
88             boolean setMtu) {
89         this.mContext = context;
90         this.mPreferences = preferences;
91         this.mEventLogger = eventLogger;
92         this.mBluetoothAdapter = bluetoothAdapter;
93         this.mToggleBluetooth = toggleBluetooth;
94         this.mAddress = address;
95         this.mTimingLogger = timingLogger;
96         this.mFastPairSignalChecker = fastPairSignalChecker;
97         this.mSetMtu = setMtu;
98     }
99 
100     /**
101      * Gets a gatt connection to address. If this connection does not exist, it creates one.
102      */
getConnection()103     BluetoothGattConnection getConnection()
104             throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
105         if (mGattConnection == null) {
106             try {
107                 mGattConnection =
108                         connect(mAddress, /* checkSignalWhenFail= */ false,
109                                 /* rescueFromError= */ null);
110             } catch (SignalLostException | SignalRotatedException e) {
111                 // Impossible to happen here because we didn't do signal check.
112                 throw new ExecutionException("getConnection throws SignalLostException", e);
113             }
114         }
115         return mGattConnection;
116     }
117 
getConnectionWithSignalLostCheck( @ullable Consumer<Integer> rescueFromError)118     BluetoothGattConnection getConnectionWithSignalLostCheck(
119             @Nullable Consumer<Integer> rescueFromError)
120             throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
121             SignalLostException, SignalRotatedException {
122         if (mGattConnection == null) {
123             mGattConnection = connect(mAddress, /* checkSignalWhenFail= */ true,
124                     rescueFromError);
125         }
126         return mGattConnection;
127     }
128 
129     /**
130      * Closes the gatt connection when it is open.
131      */
closeConnection()132     void closeConnection() throws BluetoothException {
133         if (mGattConnection != null) {
134             try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Close GATT")) {
135                 mGattConnection.close();
136                 mGattConnection = null;
137             }
138         }
139     }
140 
connect( String address, boolean checkSignalWhenFail, @Nullable Consumer<Integer> rescueFromError)141     private BluetoothGattConnection connect(
142             String address, boolean checkSignalWhenFail,
143             @Nullable Consumer<Integer> rescueFromError)
144             throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
145             SignalLostException, SignalRotatedException {
146         int i = 1;
147         boolean isRecoverable = true;
148         long startElapsedRealtime = SystemClock.elapsedRealtime();
149         BluetoothException lastException = null;
150         mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
151         while (isRecoverable) {
152             try (ScopedTiming scopedTiming =
153                     new ScopedTiming(mTimingLogger, "Connect GATT #" + i)) {
154                 Log.i(TAG, "Connecting to GATT server at " + maskBluetoothAddress(address));
155                 if (sTestMode) {
156                     return null;
157                 }
158                 BluetoothGattConnection connection =
159                         new BluetoothGattHelper(mContext, mBluetoothAdapter)
160                                 .connect(
161                                         mBluetoothAdapter.getRemoteDevice(address),
162                                         getConnectionOptions(startElapsedRealtime));
163                 connection.setOperationTimeout(
164                         TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
165                 if (mPreferences.getAutomaticallyReconnectGattWhenNeeded()) {
166                     connection.addCloseListener(
167                             () -> {
168                                 Log.i(TAG, "Gatt connection with " + maskBluetoothAddress(address)
169                                         + " closed.");
170                                 mGattConnection = null;
171                             });
172                 }
173                 mEventLogger.logCurrentEventSucceeded();
174                 if (lastException != null) {
175                     logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
176                             mEventLogger);
177                 }
178                 return connection;
179             } catch (BluetoothException e) {
180                 lastException = e;
181 
182                 boolean ableToRetry;
183                 if (mPreferences.getGattConnectRetryTimeoutMillis() > 0) {
184                     ableToRetry =
185                             (SystemClock.elapsedRealtime() - startElapsedRealtime)
186                                     < mPreferences.getGattConnectRetryTimeoutMillis();
187                     Log.i(TAG, "Retry connecting GATT by timeout: " + ableToRetry);
188                 } else {
189                     ableToRetry = i < mPreferences.getNumAttempts();
190                 }
191 
192                 if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
193                     if (isNoRetryError(mPreferences, e)) {
194                         ableToRetry = false;
195                     }
196 
197                     if (ableToRetry) {
198                         if (rescueFromError != null) {
199                             rescueFromError.accept(
200                                     e instanceof BluetoothOperationTimeoutException
201                                             ? ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT
202                                             : ErrorCode.SUCCESS_RETRY_GATT_ERROR);
203                         }
204                         if (mFastPairSignalChecker != null && checkSignalWhenFail) {
205                             FastPairDualConnection
206                                     .checkFastPairSignal(mFastPairSignalChecker, address, e);
207                         }
208                     }
209                     isRecoverable = ableToRetry;
210                     if (ableToRetry && mPreferences.getPairingRetryDelayMs() > 0) {
211                         SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
212                     }
213                 } else {
214                     isRecoverable =
215                             ableToRetry
216                                     && (e instanceof BluetoothOperationTimeoutException
217                                     || e instanceof BluetoothTimeoutException
218                                     || (e instanceof BluetoothGattException
219                                     && ((BluetoothGattException) e).getGattErrorCode() == 133));
220                 }
221                 Log.w(TAG, "GATT connect attempt " + i + "of " + mPreferences.getNumAttempts()
222                         + " failed, " + (isRecoverable ? "recovering" : "permanently"), e);
223                 if (isRecoverable) {
224                     // If we're going to retry, log failure here. If we throw, an upper level will
225                     // log it.
226                     mToggleBluetooth.toggleBluetooth();
227                     i++;
228                     mEventLogger.logCurrentEventFailed(e);
229                     mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
230                 }
231             }
232         }
233         throw checkNotNull(lastException);
234     }
235 
isNoRetryError(Preferences preferences, BluetoothException e)236     static boolean isNoRetryError(Preferences preferences, BluetoothException e) {
237         return e instanceof BluetoothGattException
238                 && preferences
239                 .getGattConnectionAndSecretHandshakeNoRetryGattError()
240                 .contains(((BluetoothGattException) e).getGattErrorCode());
241     }
242 
243     @VisibleForTesting
getTimeoutMs(long spentTime)244     long getTimeoutMs(long spentTime) {
245         long timeoutInMs;
246         if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
247             timeoutInMs =
248                     spentTime < mPreferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs()
249                             ? mPreferences.getGattConnectShortTimeoutMs()
250                             : mPreferences.getGattConnectLongTimeoutMs();
251         } else {
252             timeoutInMs = TimeUnit.SECONDS.toMillis(mPreferences.getGattConnectionTimeoutSeconds());
253         }
254         return timeoutInMs;
255     }
256 
257     private ConnectionOptions getConnectionOptions(long startElapsedRealtime) {
258         return createConnectionOptions(
259                 mSetMtu,
260                 getTimeoutMs(SystemClock.elapsedRealtime() - startElapsedRealtime));
261     }
262 
263     public static ConnectionOptions createConnectionOptions(boolean setMtu, long timeoutInMs) {
264         ConnectionOptions.Builder builder = ConnectionOptions.builder();
265         if (setMtu) {
266             // There are 3 overhead bytes added to BLE packets.
267             builder.setMtu(
268                     AES_BLOCK_LENGTH + EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH + 3);
269         }
270         builder.setConnectionTimeoutMillis(timeoutInMs);
271         return builder.build();
272     }
273 
274     @VisibleForTesting
275     void setGattConnection(BluetoothGattConnection gattConnection) {
276         this.mGattConnection = gattConnection;
277     }
278 }
279