blob: 96e829a5152788d9fedd5e2cb01e2d5178ba5077 [file] [log] [blame]
/*
* Copyright (C) 2020 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.google.android.connecteddevice.connection.spp;
import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
import static com.google.android.connecteddevice.util.SafeLog.logd;
import static com.google.android.connecteddevice.util.SafeLog.loge;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.ParcelUuid;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.connecteddevice.connection.AssociationCallback;
import com.google.android.connecteddevice.connection.AssociationSecureChannel;
import com.google.android.connecteddevice.connection.CarBluetoothManager;
import com.google.android.connecteddevice.connection.DeviceMessageStream;
import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
import com.google.android.connecteddevice.connection.SecureChannel;
import com.google.android.connecteddevice.oob.OobChannel;
import com.google.android.connecteddevice.oob.OobConnectionManager;
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener;
import com.google.android.connecteddevice.transport.spp.Connection;
import com.google.android.connecteddevice.transport.spp.PendingConnection;
import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectedListener;
import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectionErrorListener;
import com.google.android.connecteddevice.util.EventLog;
import com.google.common.base.Objects;
import java.util.UUID;
/**
* Communication manager that allows for targeted connections to a specific device from the car
* using {@link ConnectedDeviceSppDelegateBinder} .
*/
public class CarSppManager extends CarBluetoothManager {
private static final String TAG = "CarSppManager";
private final ConnectedDeviceSppDelegateBinder sppServiceBinder;
private final UUID associationServiceUuid;
private final int packetMaxBytes;
private String reconnectDeviceId;
@VisibleForTesting Connection currentConnection;
private PendingConnection currentPendingConnection;
private OobConnectionManager oobConnectionManager;
private AssociationCallback associationCallback;
/**
* Initialize a new instance of manager.
*
* @param sppBinder {@link ConnectedDeviceSppDelegateBinder} for establishing connection.
* @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
* @param packetMaxBytes Maximum size in bytes to write in one packet.
* @param enableCompression Enable compression on outgoing messages.
*/
public CarSppManager(
@NonNull ConnectedDeviceSppDelegateBinder sppBinder,
@NonNull ConnectedDeviceStorage connectedDeviceStorage,
@NonNull UUID associationServiceUuid,
int packetMaxBytes,
boolean enableCompression) {
super(connectedDeviceStorage, enableCompression);
this.sppServiceBinder = sppBinder;
this.associationServiceUuid = associationServiceUuid;
this.packetMaxBytes = packetMaxBytes;
}
@Override
public void stop() {
super.stop();
reset();
}
@Override
public void disconnectDevice(@NonNull String deviceId) {
ConnectedRemoteDevice connectedDevice = getConnectedDevice();
if (connectedDevice == null || !deviceId.equals(connectedDevice.deviceId)) {
return;
}
reset();
}
@Override
public AssociationCallback getAssociationCallback() {
return associationCallback;
}
@Override
public void setAssociationCallback(AssociationCallback callback) {
associationCallback = callback;
}
@Override
public void initiateConnectionToDevice(@NonNull UUID deviceId) {
logd(TAG, "Start spp reconnection listening for device with id: " + deviceId);
currentConnection = null;
reconnectDeviceId = deviceId.toString();
sppServiceBinder.unregisterConnectionCallback(associationServiceUuid);
sppServiceBinder.registerConnectionCallback(deviceId, reconnectOnErrorListener);
try {
PendingConnection pendingConnection =
sppServiceBinder.connectAsServer(deviceId, /* isSecure= */ true);
if (pendingConnection == null) {
return;
}
currentPendingConnection = pendingConnection;
pendingConnection
.setOnConnectedListener(reconnectOnConnectedListener)
.setOnConnectionErrorListener(onConnectionErrorListener);
} catch (RemoteException e) {
loge(TAG, "Error when start connection with remote device as server.", e);
}
}
@Override
public void reset() {
super.reset();
reconnectDeviceId = null;
associationCallback = null;
if (currentConnection != null) {
try {
sppServiceBinder.disconnect(currentConnection);
} catch (RemoteException e) {
loge(TAG, "Error when try to disconnect with remote device.", e);
}
currentConnection = null;
}
if (currentPendingConnection != null) {
try {
sppServiceBinder.cancelConnectionAttempt(currentPendingConnection);
} catch (RemoteException e) {
loge(TAG, "Error when try to disconnect with remote device.", e);
}
currentPendingConnection = null;
}
}
/** Start the association by listening to incoming connect request. */
@Override
public void startAssociation(
@NonNull String nameForAssociation, @NonNull AssociationCallback callback) {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
loge(TAG, "Bluetooth is unavailable on this device. Unable to start associating.");
return;
}
if (reconnectDeviceId != null) {
sppServiceBinder.unregisterConnectionCallback(UUID.fromString(reconnectDeviceId));
}
reset();
associationCallback = callback;
sppServiceBinder.registerConnectionCallback(associationServiceUuid, associationOnErrorListener);
try {
PendingConnection pendingConnection =
sppServiceBinder.connectAsServer(associationServiceUuid, /* isSecure= */ true);
if (pendingConnection == null) {
callback.onAssociationStartFailure();
} else {
currentPendingConnection = pendingConnection;
pendingConnection
.setOnConnectedListener(associationOnConnectedListener)
.setOnConnectionErrorListener(onConnectionErrorListener);
callback.onAssociationStartSuccess(/* deviceName= */ null);
}
} catch (RemoteException e) {
callback.onAssociationStartFailure();
loge(TAG, "Error when try to start associate with remote device.", e);
}
}
/** Start the association with a new device using out of band verification code exchange */
@Override
public void startOutOfBandAssociation(
@NonNull String nameForAssociation,
@NonNull OobChannel oobChannel,
@NonNull AssociationCallback callback) {
logd(TAG, "Starting out of band association.");
startAssociation(
nameForAssociation,
new AssociationCallback() {
@Override
public void onAssociationStartSuccess(String deviceName) {
associationCallback = callback;
boolean success = oobConnectionManager.startOobExchange(oobChannel);
if (!success) {
callback.onAssociationStartFailure();
return;
}
callback.onAssociationStartSuccess(deviceName);
}
@Override
public void onAssociationStartFailure() {
callback.onAssociationStartFailure();
}
});
oobConnectionManager = new OobConnectionManager();
}
private void onDeviceConnected(Connection connection, boolean isReconnect) {
onDeviceConnected(connection, isReconnect, /* isOob= */ false);
}
private void onDeviceConnected(Connection connection, boolean isReconnect, boolean isOob) {
currentConnection = connection;
currentPendingConnection = null;
EventLog.onDeviceConnected();
BluetoothDevice device = connection.getRemoteDevice();
setClientDeviceAddress(device.getAddress());
setClientDeviceName(connection.getRemoteDeviceName());
DeviceMessageStream secureStream =
new SppDeviceMessageStream(sppServiceBinder, connection, packetMaxBytes);
secureStream.setMessageReceivedErrorListener(
exception -> {
disconnectWithError("Error occurred in stream: " + exception.getMessage(), exception);
});
SecureChannel secureChannel;
if (isReconnect) {
secureChannel =
new ReconnectSecureChannel(
secureStream, storage, reconnectDeviceId, /* expectedChallengeResponse= */ null);
} else if (isOob) {
// TODO(b/160901821): Integrate Oob with Spp channel
loge(TAG, "Oob verification is currently not available for Spp");
return;
} else {
secureChannel = new AssociationSecureChannel(secureStream, storage);
}
secureChannel.registerCallback(secureChannelCallback);
ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
connectedDevice.secureChannel = secureChannel;
addConnectedDevice(connectedDevice);
if (isReconnect) {
setDeviceIdAndNotifyCallbacks(reconnectDeviceId);
reconnectDeviceId = null;
}
}
private final OnConnectedListener reconnectOnConnectedListener =
new OnConnectedListener() {
@Override
public void onConnected(
UUID uuid, BluetoothDevice remoteDevice, boolean isSecure, String deviceName) {
Connection connection =
new Connection(new ParcelUuid(uuid), remoteDevice, isSecure, deviceName);
onDeviceConnected(connection, /* isReconnect= */ true);
}
};
@VisibleForTesting
final OnErrorListener reconnectOnErrorListener =
new OnErrorListener() {
@Override
public void onError(Connection connection) {
if (!Objects.equal(currentConnection, connection)) {
loge(TAG, "Receive connection error callback on a unrecognized connection, ignored.");
return;
}
currentConnection = null;
ConnectedRemoteDevice connectedDevice = getConnectedDevice(connection.getRemoteDevice());
// Reset before invoking callbacks to avoid a race condition with reconnect
// logic.
reset();
String deviceId = connectedDevice == null ? reconnectDeviceId : connectedDevice.deviceId;
if (deviceId != null) {
logd(TAG, "Connected device " + deviceId + " disconnected.");
callbacks.invoke(callback -> callback.onDeviceDisconnected(deviceId));
}
}
};
private final OnConnectedListener associationOnConnectedListener =
new OnConnectedListener() {
@Override
public void onConnected(
UUID uuid, BluetoothDevice remoteDevice, boolean isSecure, String deviceName) {
Connection connection =
new Connection(new ParcelUuid(uuid), remoteDevice, isSecure, deviceName);
onDeviceConnected(connection, /* isReconnect= */ false);
ConnectedRemoteDevice connectedDevice = getConnectedDevice();
if (connectedDevice == null || connectedDevice.secureChannel == null) {
loge(TAG, "No connected device or secure channel found when trying to associate.");
return;
}
((AssociationSecureChannel) connectedDevice.secureChannel)
.setShowVerificationCodeListener(
code -> {
if (associationCallback == null) {
loge(TAG, "No valid callback for association.");
return;
}
associationCallback.onVerificationCodeAvailable(code);
});
}
};
final OnConnectionErrorListener onConnectionErrorListener =
new OnConnectionErrorListener() {
@Override
public void onConnectionError() {
loge(TAG, "Connection attempt failed when performing a server role.");
}
};
@VisibleForTesting
final OnErrorListener associationOnErrorListener =
new OnErrorListener() {
@Override
public void onError(Connection connection) {
if (!Objects.equal(currentConnection, connection)) {
loge(TAG, "Receive connection error callback on a unrecognized connection, ignored.");
return;
}
currentConnection = null;
if (isAssociating()) {
associationCallback.onAssociationError(DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
}
ConnectedRemoteDevice connectedDevice = getConnectedDevice(connection.getRemoteDevice());
// Reset before invoking callbacks to avoid a race condition with reconnect
// logic.
reset();
if (connectedDevice != null && connectedDevice.deviceId != null) {
callbacks.invoke(callback -> callback.onDeviceDisconnected(connectedDevice.deviceId));
}
}
};
}