blob: 8f219bcacfcb57320e2d302ebf581269e991d738 [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.oob;
import static com.google.android.connecteddevice.util.SafeLog.logd;
import static com.google.android.connecteddevice.util.SafeLog.loge;
import static com.google.android.connecteddevice.util.SafeLog.logw;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelUuid;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.connecteddevice.transport.BluetoothDeviceProvider;
import com.google.android.connecteddevice.transport.ConnectionProtocol;
import com.google.android.connecteddevice.transport.ProtocolDevice;
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
import com.google.android.connecteddevice.transport.spp.Connection;
import com.google.android.connecteddevice.transport.spp.PendingConnection;
import com.google.android.connecteddevice.transport.spp.PendingSentMessage;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/** Handles out of band data exchange over a secure RFCOMM channel. */
public class BluetoothRfcommChannel implements OobChannel {
private static final String TAG = "BluetoothRfcommChannel";
// TODO(b/159500330): Generate random UUID.
private static final UUID RFCOMM_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final int CONNECTION_TIMEOUT_MS = 500;
private final AtomicBoolean isInterrupted = new AtomicBoolean();
private final ConnectedDeviceSppDelegateBinder sppDelegateBinder;
private Connection activeConnection;
private PendingConnection activePendingConnection;
@VisibleForTesting Callback callback;
public BluetoothRfcommChannel(ConnectedDeviceSppDelegateBinder sppDelegateBinder) {
this.sppDelegateBinder = sppDelegateBinder;
}
@Override
public boolean completeOobDataExchange(
@NonNull ProtocolDevice device, @NonNull Callback callback) {
ConnectionProtocol protocol = device.getProtocol();
if (!(protocol instanceof BluetoothDeviceProvider)) {
logw(TAG, "Protocol is not supported by current OOB channel, ignored.");
return false;
}
BluetoothDevice remoteDevice =
((BluetoothDeviceProvider) protocol).getBluetoothDeviceById(device.getProtocolId());
completeOobDataExchange(
remoteDevice, callback, () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
return true;
}
@VisibleForTesting
void completeOobDataExchange(
BluetoothDevice remoteDevice,
Callback callback,
BondedDevicesResolver bondedDevicesResolver) {
this.callback = callback;
Set<BluetoothDevice> bondedDevices = bondedDevicesResolver.getBondedDevices();
if (bondedDevices == null || !bondedDevices.contains(remoteDevice)) {
notifyFailure(
"This device has not been bonded to device with address " + remoteDevice.getAddress(),
/* exception= */ null);
return;
}
try {
PendingConnection connection =
sppDelegateBinder.connectAsClient(RFCOMM_UUID, remoteDevice, /* isSecure= */ true);
if (connection == null) {
notifyFailure(
"Connection with " + remoteDevice.getName() + " failed.", /* exception= */ null);
return;
}
activePendingConnection = connection;
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(
() -> {
logd(TAG, "Cancelling connection with " + remoteDevice.getName());
try {
sppDelegateBinder.cancelConnectionAttempt(connection);
} catch (RemoteException e) {
logw(TAG, "Failed to cancel connection attempt with " + remoteDevice.getName());
}
notifyFailure(
"Connection with " + remoteDevice.getName() + " timed out.", /* exception= */ null);
},
CONNECTION_TIMEOUT_MS);
connection
.setOnConnectedListener(
(uuid, btDevice, isSecure, deviceName) -> {
handler.removeCallbacksAndMessages(null);
activePendingConnection = null;
activeConnection =
new Connection(new ParcelUuid(uuid), btDevice, isSecure, deviceName);
notifySuccess();
})
.setOnConnectionErrorListener(
() -> {
handler.removeCallbacksAndMessages(null);
notifyFailure(
"Connection with " + remoteDevice.getName() + " failed.",
/* exception= */ null);
});
} catch (RemoteException e) {
notifyFailure("Connection with " + remoteDevice.getName() + " failed.", e);
}
}
@Override
public void sendOobData(byte[] oobData) {
if (isInterrupted.get()) {
logd(TAG, "Oob connection is interrupted, data will not be set.");
return;
}
if (activeConnection == null) {
notifyFailure("Connection is null, oob data cannot be sent", /* exception= */ null);
return;
}
try {
PendingSentMessage pendingSentMessage =
sppDelegateBinder.sendMessage(activeConnection, oobData);
if (pendingSentMessage == null) {
notifyFailure("Sending oob data failed", null);
return;
}
pendingSentMessage.setOnSuccessListener(
() -> {
try {
disconnect();
} catch (RemoteException e) {
logw(TAG, "Sending oob data succeeded, but disconnect failed");
}
});
} catch (RemoteException e) {
notifyFailure("Sending oob data failed", e);
}
}
@Override
public void interrupt() {
logd(TAG, "Interrupt received.");
isInterrupted.set(true);
try {
disconnect();
} catch (RemoteException e) {
loge(TAG, "Disconnect failed", e);
}
}
private void disconnect() throws RemoteException {
if (activeConnection != null) {
sppDelegateBinder.disconnect(activeConnection);
activeConnection = null;
}
if (activePendingConnection != null) {
sppDelegateBinder.cancelConnectionAttempt(activePendingConnection);
activePendingConnection = null;
}
}
private void notifyFailure(@NonNull String message, @Nullable Exception exception) {
loge(TAG, message, exception);
if (callback != null && !isInterrupted.get()) {
callback.onOobExchangeFailure();
}
}
private void notifySuccess() {
if (callback != null && !isInterrupted.get()) {
callback.onOobExchangeSuccess();
}
}
/**
* Interface for determining all the devices that the current device is Bluetooth bonded to, used
* for testing.
*/
interface BondedDevicesResolver {
Set<BluetoothDevice> getBondedDevices();
}
}