| /* |
| * 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.android.car.connecteddevice.connection.spp; |
| |
| import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION; |
| import static com.android.car.connecteddevice.util.SafeLog.logd; |
| import static com.android.car.connecteddevice.util.SafeLog.loge; |
| |
| import android.annotation.NonNull; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| |
| import com.android.car.connecteddevice.AssociationCallback; |
| import com.android.car.connecteddevice.connection.AssociationSecureChannel; |
| import com.android.car.connecteddevice.connection.CarBluetoothManager; |
| import com.android.car.connecteddevice.connection.DeviceMessageStream; |
| import com.android.car.connecteddevice.connection.ReconnectSecureChannel; |
| import com.android.car.connecteddevice.connection.SecureChannel; |
| import com.android.car.connecteddevice.oob.OobChannel; |
| import com.android.car.connecteddevice.oob.OobConnectionManager; |
| import com.android.car.connecteddevice.storage.ConnectedDeviceStorage; |
| import com.android.car.connecteddevice.util.EventLog; |
| |
| import java.util.UUID; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.Executors; |
| |
| |
| /** |
| * Communication manager that allows for targeted connections to a specific device from the car |
| * using {@link SppManager} . |
| */ |
| public class CarSppManager extends CarBluetoothManager { |
| |
| private static final String TAG = "CarSppManager"; |
| |
| private final SppManager mSppManager; |
| |
| private final UUID mAssociationServiceUuid; |
| |
| private String mReconnectDeviceId; |
| |
| private OobConnectionManager mOobConnectionManager; |
| |
| private Executor mCallbackExecutor; |
| private AssociationCallback mAssociationCallback; |
| |
| /** |
| * Initialize a new instance of manager. |
| * |
| * @param sppManager {@link SppManager} for establishing connection. |
| * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features. |
| */ |
| public CarSppManager(@NonNull SppManager sppManager, |
| @NonNull ConnectedDeviceStorage connectedDeviceStorage, |
| @NonNull UUID associationServiceUuid) { |
| super(connectedDeviceStorage); |
| mSppManager = sppManager; |
| mCallbackExecutor = Executors.newSingleThreadExecutor(); |
| mAssociationServiceUuid = associationServiceUuid; |
| } |
| |
| @Override |
| public void stop() { |
| super.stop(); |
| reset(); |
| } |
| |
| @Override |
| public void disconnectDevice(@NonNull String deviceId) { |
| ConnectedRemoteDevice connectedDevice = getConnectedDevice(); |
| if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) { |
| return; |
| } |
| reset(); |
| } |
| |
| @Override |
| public AssociationCallback getAssociationCallback() { |
| return mAssociationCallback; |
| } |
| |
| @Override |
| public void setAssociationCallback(AssociationCallback callback) { |
| mAssociationCallback = callback; |
| } |
| |
| @Override |
| public void initiateConnectionToDevice(@NonNull UUID deviceId) { |
| logd(TAG, "Start spp reconnection listening for device with id: " + deviceId.toString()); |
| mReconnectDeviceId = deviceId.toString(); |
| mSppManager.unregisterCallback(mAssociationSppCallback); |
| mSppManager.registerCallback(mReconnectSppCallback, mCallbackExecutor); |
| mSppManager.startListening(deviceId); |
| } |
| |
| @Override |
| public void reset() { |
| super.reset(); |
| mReconnectDeviceId = null; |
| mAssociationCallback = null; |
| mSppManager.cleanup(); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| reset(); |
| mAssociationCallback = callback; |
| mSppManager.unregisterCallback(mReconnectSppCallback); |
| mSppManager.registerCallback(mAssociationSppCallback, mCallbackExecutor); |
| if (mSppManager.startListening(mAssociationServiceUuid)) { |
| callback.onAssociationStartSuccess(adapter.getName()); |
| } else { |
| callback.onAssociationStartFailure(); |
| } |
| } |
| |
| /** |
| * 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) { |
| mAssociationCallback = callback; |
| boolean success = mOobConnectionManager.startOobExchange(oobChannel); |
| if (!success) { |
| callback.onAssociationStartFailure(); |
| return; |
| } |
| callback.onAssociationStartSuccess(deviceName); |
| } |
| |
| @Override |
| public void onAssociationStartFailure() { |
| callback.onAssociationStartFailure(); |
| } |
| }); |
| mOobConnectionManager = new OobConnectionManager(); |
| } |
| |
| private void onDeviceConnected(BluetoothDevice device, boolean isReconnect) { |
| onDeviceConnected(device, isReconnect, /* isOob= */ false); |
| } |
| |
| private void onDeviceConnected(BluetoothDevice device, boolean isReconnect, boolean isOob) { |
| EventLog.onDeviceConnected(); |
| setClientDeviceAddress(device.getAddress()); |
| setClientDeviceName(device.getName()); |
| DeviceMessageStream secureStream = new SppDeviceMessageStream(mSppManager, device); |
| secureStream.setMessageReceivedErrorListener( |
| exception -> { |
| disconnectWithError("Error occurred in stream: " + exception.getMessage(), |
| exception); |
| }); |
| SecureChannel secureChannel; |
| // TODO(b/157492943): Define an out of band version of ReconnectSecureChannel |
| if (isReconnect) { |
| secureChannel = new ReconnectSecureChannel(secureStream, mStorage, mReconnectDeviceId, |
| /* 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, mStorage); |
| } |
| secureChannel.registerCallback(mSecureChannelCallback); |
| ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null); |
| connectedDevice.mSecureChannel = secureChannel; |
| addConnectedDevice(connectedDevice); |
| if (isReconnect) { |
| setDeviceIdAndNotifyCallbacks(mReconnectDeviceId); |
| mReconnectDeviceId = null; |
| } |
| } |
| |
| private final SppManager.ConnectionCallback mReconnectSppCallback = |
| new SppManager.ConnectionCallback() { |
| @Override |
| public void onRemoteDeviceConnected(BluetoothDevice device) { |
| onDeviceConnected(device, /* isReconnect= */ true); |
| } |
| |
| @Override |
| public void onRemoteDeviceDisconnected(BluetoothDevice device) { |
| ConnectedRemoteDevice connectedDevice = getConnectedDevice(device); |
| // Reset before invoking callbacks to avoid a race condition with reconnect |
| // logic. |
| reset(); |
| String deviceId = connectedDevice == null ? mReconnectDeviceId |
| : connectedDevice.mDeviceId; |
| if (deviceId != null) { |
| logd(TAG, "Connected device " + deviceId + " disconnected."); |
| mCallbacks.invoke(callback -> callback.onDeviceDisconnected(deviceId)); |
| } |
| } |
| }; |
| |
| private final SppManager.ConnectionCallback mAssociationSppCallback = |
| new SppManager.ConnectionCallback() { |
| @Override |
| public void onRemoteDeviceConnected(BluetoothDevice device) { |
| onDeviceConnected(device, /* isReconnect= */ false); |
| ConnectedRemoteDevice connectedDevice = getConnectedDevice(); |
| if (connectedDevice == null || connectedDevice.mSecureChannel == null) { |
| loge(TAG, |
| "No connected device or secure channel found when try to " |
| + "associate."); |
| return; |
| } |
| ((AssociationSecureChannel) connectedDevice.mSecureChannel) |
| .setShowVerificationCodeListener( |
| code -> { |
| if (mAssociationCallback == null) { |
| loge(TAG, "No valid callback for association."); |
| return; |
| } |
| mAssociationCallback.onVerificationCodeAvailable(code); |
| }); |
| } |
| |
| @Override |
| public void onRemoteDeviceDisconnected(BluetoothDevice device) { |
| ConnectedRemoteDevice connectedDevice = getConnectedDevice(device); |
| if (isAssociating()) { |
| mAssociationCallback.onAssociationError( |
| DEVICE_ERROR_UNEXPECTED_DISCONNECTION); |
| } |
| // Reset before invoking callbacks to avoid a race condition with reconnect |
| // logic. |
| reset(); |
| if (connectedDevice != null && connectedDevice.mDeviceId != null) { |
| mCallbacks.invoke(callback -> callback.onDeviceDisconnected( |
| connectedDevice.mDeviceId)); |
| } |
| } |
| }; |
| } |