blob: e7ce4bfde675a4e45db4777aaf42542ec24a32b0 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.nearby.common.bluetooth.fastpair;
import static com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Consumer;
import com.android.server.nearby.common.bluetooth.BluetoothException;
import com.android.server.nearby.common.bluetooth.BluetoothGattException;
import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException;
import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming;
import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper;
import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode;
import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Manager for working with Gatt connections.
*
* <p>This helper class allows for opening and closing GATT connections to a provided address.
* Optionally, it can also support automatically reopening a connection in the case that it has been
* closed when it's next needed through {@link Preferences#getAutomaticallyReconnectGattWhenNeeded}.
*/
// TODO(b/202524672): Add class unit test.
final class GattConnectionManager {
private static final String TAG = GattConnectionManager.class.getSimpleName();
private final Context mContext;
private final Preferences mPreferences;
private final EventLoggerWrapper mEventLogger;
private final BluetoothAdapter mBluetoothAdapter;
private final ToggleBluetoothTask mToggleBluetooth;
private final String mAddress;
private final TimingLogger mTimingLogger;
private final boolean mSetMtu;
@Nullable
private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
@Nullable
private BluetoothGattConnection mGattConnection;
private static boolean sTestMode = false;
static void enableTestMode() {
sTestMode = true;
}
GattConnectionManager(
Context context,
Preferences preferences,
EventLoggerWrapper eventLogger,
BluetoothAdapter bluetoothAdapter,
ToggleBluetoothTask toggleBluetooth,
String address,
TimingLogger timingLogger,
@Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker,
boolean setMtu) {
this.mContext = context;
this.mPreferences = preferences;
this.mEventLogger = eventLogger;
this.mBluetoothAdapter = bluetoothAdapter;
this.mToggleBluetooth = toggleBluetooth;
this.mAddress = address;
this.mTimingLogger = timingLogger;
this.mFastPairSignalChecker = fastPairSignalChecker;
this.mSetMtu = setMtu;
}
/**
* Gets a gatt connection to address. If this connection does not exist, it creates one.
*/
BluetoothGattConnection getConnection()
throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
if (mGattConnection == null) {
try {
mGattConnection =
connect(mAddress, /* checkSignalWhenFail= */ false,
/* rescueFromError= */ null);
} catch (SignalLostException | SignalRotatedException e) {
// Impossible to happen here because we didn't do signal check.
throw new ExecutionException("getConnection throws SignalLostException", e);
}
}
return mGattConnection;
}
BluetoothGattConnection getConnectionWithSignalLostCheck(
@Nullable Consumer<Integer> rescueFromError)
throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
SignalLostException, SignalRotatedException {
if (mGattConnection == null) {
mGattConnection = connect(mAddress, /* checkSignalWhenFail= */ true,
rescueFromError);
}
return mGattConnection;
}
/**
* Closes the gatt connection when it is open.
*/
void closeConnection() throws BluetoothException {
if (mGattConnection != null) {
try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Close GATT")) {
mGattConnection.close();
mGattConnection = null;
}
}
}
private BluetoothGattConnection connect(
String address, boolean checkSignalWhenFail,
@Nullable Consumer<Integer> rescueFromError)
throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
SignalLostException, SignalRotatedException {
int i = 1;
boolean isRecoverable = true;
long startElapsedRealtime = SystemClock.elapsedRealtime();
BluetoothException lastException = null;
mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
while (isRecoverable) {
try (ScopedTiming scopedTiming =
new ScopedTiming(mTimingLogger, "Connect GATT #" + i)) {
Log.i(TAG, "Connecting to GATT server at " + maskBluetoothAddress(address));
if (sTestMode) {
return null;
}
BluetoothGattConnection connection =
new BluetoothGattHelper(mContext, mBluetoothAdapter)
.connect(
mBluetoothAdapter.getRemoteDevice(address),
getConnectionOptions(startElapsedRealtime));
connection.setOperationTimeout(
TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
if (mPreferences.getAutomaticallyReconnectGattWhenNeeded()) {
connection.addCloseListener(
() -> {
Log.i(TAG, "Gatt connection with " + maskBluetoothAddress(address)
+ " closed.");
mGattConnection = null;
});
}
mEventLogger.logCurrentEventSucceeded();
if (lastException != null) {
logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
mEventLogger);
}
return connection;
} catch (BluetoothException e) {
lastException = e;
boolean ableToRetry;
if (mPreferences.getGattConnectRetryTimeoutMillis() > 0) {
ableToRetry =
(SystemClock.elapsedRealtime() - startElapsedRealtime)
< mPreferences.getGattConnectRetryTimeoutMillis();
Log.i(TAG, "Retry connecting GATT by timeout: " + ableToRetry);
} else {
ableToRetry = i < mPreferences.getNumAttempts();
}
if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
if (isNoRetryError(mPreferences, e)) {
ableToRetry = false;
}
if (ableToRetry) {
if (rescueFromError != null) {
rescueFromError.accept(
e instanceof BluetoothOperationTimeoutException
? ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT
: ErrorCode.SUCCESS_RETRY_GATT_ERROR);
}
if (mFastPairSignalChecker != null && checkSignalWhenFail) {
FastPairDualConnection
.checkFastPairSignal(mFastPairSignalChecker, address, e);
}
}
isRecoverable = ableToRetry;
if (ableToRetry && mPreferences.getPairingRetryDelayMs() > 0) {
SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
}
} else {
isRecoverable =
ableToRetry
&& (e instanceof BluetoothOperationTimeoutException
|| e instanceof BluetoothTimeoutException
|| (e instanceof BluetoothGattException
&& ((BluetoothGattException) e).getGattErrorCode() == 133));
}
Log.w(TAG, "GATT connect attempt " + i + "of " + mPreferences.getNumAttempts()
+ " failed, " + (isRecoverable ? "recovering" : "permanently"), e);
if (isRecoverable) {
// If we're going to retry, log failure here. If we throw, an upper level will
// log it.
mToggleBluetooth.toggleBluetooth();
i++;
mEventLogger.logCurrentEventFailed(e);
mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
}
}
}
throw checkNotNull(lastException);
}
static boolean isNoRetryError(Preferences preferences, BluetoothException e) {
return e instanceof BluetoothGattException
&& preferences
.getGattConnectionAndSecretHandshakeNoRetryGattError()
.contains(((BluetoothGattException) e).getGattErrorCode());
}
@VisibleForTesting
long getTimeoutMs(long spentTime) {
long timeoutInMs;
if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
timeoutInMs =
spentTime < mPreferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs()
? mPreferences.getGattConnectShortTimeoutMs()
: mPreferences.getGattConnectLongTimeoutMs();
} else {
timeoutInMs = TimeUnit.SECONDS.toMillis(mPreferences.getGattConnectionTimeoutSeconds());
}
return timeoutInMs;
}
private ConnectionOptions getConnectionOptions(long startElapsedRealtime) {
return createConnectionOptions(
mSetMtu,
getTimeoutMs(SystemClock.elapsedRealtime() - startElapsedRealtime));
}
public static ConnectionOptions createConnectionOptions(boolean setMtu, long timeoutInMs) {
ConnectionOptions.Builder builder = ConnectionOptions.builder();
if (setMtu) {
// There are 3 overhead bytes added to BLE packets.
builder.setMtu(
AES_BLOCK_LENGTH + EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH + 3);
}
builder.setConnectionTimeoutMillis(timeoutInMs);
return builder.build();
}
@VisibleForTesting
void setGattConnection(BluetoothGattConnection gattConnection) {
this.mGattConnection = gattConnection;
}
}