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