1 /* 2 * Copyright (C) 2019 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.car.trust; 18 19 import android.annotation.NonNull; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothGattCharacteristic; 22 import android.os.Handler; 23 import android.util.Log; 24 25 import com.android.car.BLEStreamProtos.BLEMessageProto.BLEMessage; 26 import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType; 27 import com.android.car.protobuf.InvalidProtocolBufferException; 28 import com.android.internal.annotations.VisibleForTesting; 29 30 import java.io.IOException; 31 import java.util.ArrayDeque; 32 import java.util.ArrayList; 33 import java.util.Deque; 34 import java.util.List; 35 import java.util.concurrent.TimeUnit; 36 37 /** 38 * Version 1 of the message stream. 39 */ 40 class BleMessageStreamV1 implements BleMessageStream { 41 private static final String TAG = "BleMessageStreamV1"; 42 43 @VisibleForTesting 44 static final int BLE_MESSAGE_RETRY_LIMIT = 5; 45 46 /** 47 * The delay in milliseconds before a failed message is retried for sending. 48 * 49 * <p>This delay is only present for a message has been chunked. Each part of this chunked 50 * message requires an ACK from the remote device before the next chunk is sent. If this ACK is 51 * not received, then a second message is sent after this delay. 52 * 53 * <p>The value of this delay is 2 seconds to allow for 1 second to notify the remote device of 54 * a new message and 1 second to wait for an ACK. 55 */ 56 private static final long BLE_MESSAGE_RETRY_DELAY_MS = TimeUnit.SECONDS.toMillis(2); 57 58 private final Handler mHandler; 59 private final BlePeripheralManager mBlePeripheralManager; 60 private final BluetoothDevice mDevice; 61 private final BluetoothGattCharacteristic mWriteCharacteristic; 62 private final BluetoothGattCharacteristic mReadCharacteristic; 63 64 // Explicitly using an ArrayDequeue here for performance when used as a queue. 65 private final Deque<BLEMessage> mMessageQueue = new ArrayDeque<>(); 66 private final BLEMessagePayloadStream mPayloadStream = new BLEMessagePayloadStream(); 67 68 /** The number of times that a message to send has been retried. */ 69 private int mRetryCount = 0; 70 71 /** 72 * The maximum write size for a single message. 73 * 74 * <p>By default, this value is 20 because the smaller possible write size over BLE is 23 bytes. 75 * However, 3 bytes need to be subtracted due to them being used by the header of the BLE 76 * packet. Thus, the final value is 20. 77 */ 78 private int mMaxWriteSize = 20; 79 80 private final List<BleMessageStreamCallback> mCallbacks = new ArrayList<>(); 81 BleMessageStreamV1(@onNull Handler handler, @NonNull BlePeripheralManager blePeripheralManager, @NonNull BluetoothDevice device, @NonNull BluetoothGattCharacteristic writeCharacteristic, @NonNull BluetoothGattCharacteristic readCharacteristic)82 BleMessageStreamV1(@NonNull Handler handler, @NonNull BlePeripheralManager blePeripheralManager, 83 @NonNull BluetoothDevice device, 84 @NonNull BluetoothGattCharacteristic writeCharacteristic, 85 @NonNull BluetoothGattCharacteristic readCharacteristic) { 86 mHandler = handler; 87 mBlePeripheralManager = blePeripheralManager; 88 mDevice = device; 89 mWriteCharacteristic = writeCharacteristic; 90 mReadCharacteristic = readCharacteristic; 91 92 mBlePeripheralManager.addOnCharacteristicWriteListener(this::onCharacteristicWrite); 93 } 94 95 /** Registers the given callback to be notified of various events within the stream. */ 96 @Override registerCallback(@onNull BleMessageStreamCallback callback)97 public void registerCallback(@NonNull BleMessageStreamCallback callback) { 98 mCallbacks.add(callback); 99 } 100 101 /** Unregisters the given callback from being notified of stream events. */ 102 @Override unregisterCallback(@onNull BleMessageStreamCallback callback)103 public void unregisterCallback(@NonNull BleMessageStreamCallback callback) { 104 mCallbacks.remove(callback); 105 } 106 107 /** Sets the maximum size of a message that can be sent. */ 108 @Override setMaxWriteSize(int maxWriteSize)109 public void setMaxWriteSize(int maxWriteSize) { 110 mMaxWriteSize = maxWriteSize; 111 } 112 113 /** Returns the maximum size of a message that can be sent. */ 114 @Override getMaxWriteSize()115 public int getMaxWriteSize() { 116 return mMaxWriteSize; 117 } 118 119 /** 120 * Writes the given message to the write characteristic of this stream. 121 * 122 * <p>This method will handle the chunking of messages based on maximum write size assigned to 123 * this stream.. If there is an error during the send, any callbacks on this stream will be 124 * notified of the error. 125 * 126 * @param message The message to send. 127 * @param operationType The {@link OperationType} of this message. 128 * @param isPayloadEncrypted {@code true} if the message to send has been encrypted. 129 */ 130 @Override writeMessage(@onNull byte[] message, @NonNull OperationType operationType, boolean isPayloadEncrypted)131 public void writeMessage(@NonNull byte[] message, @NonNull OperationType operationType, 132 boolean isPayloadEncrypted) { 133 if (Log.isLoggable(TAG, Log.DEBUG)) { 134 Log.d(TAG, "Writing message to device with name: " + mDevice.getName()); 135 } 136 137 List<BLEMessage> bleMessages = BLEMessageV1Factory.makeBLEMessages(message, operationType, 138 mMaxWriteSize, isPayloadEncrypted); 139 140 if (Log.isLoggable(TAG, Log.DEBUG)) { 141 Log.d(TAG, "Number of messages to send to device: " + bleMessages.size()); 142 } 143 144 // Each write will override previous messages. 145 if (!mMessageQueue.isEmpty()) { 146 mMessageQueue.clear(); 147 Log.w(TAG, "Request to write a new message when there are still messages in the " 148 + "queue."); 149 } 150 151 mMessageQueue.addAll(bleMessages); 152 153 writeNextMessageInQueue(); 154 } 155 156 /** 157 * Processes a message from the client and notifies any callbacks of the success of this 158 * call. 159 */ 160 @VisibleForTesting onCharacteristicWrite(@onNull BluetoothDevice device, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value)161 void onCharacteristicWrite(@NonNull BluetoothDevice device, 162 @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) { 163 if (!mDevice.equals(device)) { 164 Log.w(TAG, "Received a message from a device (" + device.getAddress() + ") that is not " 165 + "the expected device (" + mDevice.getAddress() + ") registered to this " 166 + "stream. Ignoring."); 167 return; 168 } 169 170 if (!characteristic.getUuid().equals(mReadCharacteristic.getUuid())) { 171 Log.w(TAG, "Received a write to a characteristic (" + characteristic.getUuid() 172 + ") that is not the expected UUID (" + mReadCharacteristic.getUuid() 173 + "). Ignoring."); 174 return; 175 } 176 177 BLEMessage bleMessage; 178 try { 179 bleMessage = BLEMessage.parseFrom(value); 180 } catch (InvalidProtocolBufferException e) { 181 Log.e(TAG, "Can not parse BLE message from client.", e); 182 183 for (BleMessageStreamCallback callback : mCallbacks) { 184 callback.onMessageReceivedError(characteristic.getUuid()); 185 } 186 return; 187 } 188 189 if (bleMessage.getOperation() == OperationType.ACK) { 190 handleClientAckMessage(); 191 return; 192 } 193 194 try { 195 mPayloadStream.write(bleMessage); 196 } catch (IOException e) { 197 Log.e(TAG, "Unable to parse the BLE message's payload from client.", e); 198 199 for (BleMessageStreamCallback callback : mCallbacks) { 200 callback.onMessageReceivedError(characteristic.getUuid()); 201 } 202 return; 203 } 204 205 // If it's not complete, make sure the client knows that this message was received. 206 if (!mPayloadStream.isComplete()) { 207 sendAcknowledgmentMessage(); 208 return; 209 } 210 211 for (BleMessageStreamCallback callback : mCallbacks) { 212 callback.onMessageReceived(mPayloadStream.toByteArray(), characteristic.getUuid()); 213 } 214 215 mPayloadStream.reset(); 216 } 217 218 /** 219 * Writes the next message in the message queue to the write characteristic. 220 * 221 * <p>If the message queue is empty, then this method will do nothing. 222 */ writeNextMessageInQueue()223 private void writeNextMessageInQueue() { 224 // This should not happen in practice since this method is private and should only be called 225 // for a non-empty queue. 226 if (mMessageQueue.isEmpty()) { 227 Log.e(TAG, "Call to write next message in queue, but the message queue is empty."); 228 return; 229 } 230 231 if (mMessageQueue.size() == 1) { 232 writeValueAndNotify(mMessageQueue.remove().toByteArray()); 233 return; 234 } 235 236 mHandler.post(mSendMessageWithTimeoutRunnable); 237 } 238 handleClientAckMessage()239 private void handleClientAckMessage() { 240 if (Log.isLoggable(TAG, Log.DEBUG)) { 241 Log.d(TAG, "Received ACK from client. Attempting to write next message in queue."); 242 } 243 244 mHandler.removeCallbacks(mSendMessageWithTimeoutRunnable); 245 mRetryCount = 0; 246 247 if (mMessageQueue.isEmpty()) { 248 Log.e(TAG, "Received ACK, but the message queue is empty. Ignoring."); 249 return; 250 } 251 252 // Previous message has been sent successfully so we can start the next message. 253 mMessageQueue.remove(); 254 writeNextMessageInQueue(); 255 } 256 sendAcknowledgmentMessage()257 private void sendAcknowledgmentMessage() { 258 writeValueAndNotify(BLEMessageV1Factory.makeAcknowledgementMessage().toByteArray()); 259 } 260 261 /** 262 * Convenience method to write the given message to the {@link #mWriteCharacteristic} of this 263 * class. After writing, this method will also send notifications to any listening devices that 264 * the write was made. 265 */ writeValueAndNotify(@onNull byte[] message)266 private void writeValueAndNotify(@NonNull byte[] message) { 267 mWriteCharacteristic.setValue(message); 268 269 mBlePeripheralManager.notifyCharacteristicChanged(mDevice, mWriteCharacteristic, 270 /* confirm= */ false); 271 } 272 273 /** 274 * A runnable that will write the message at the head of the {@link #mMessageQueue} and set up 275 * a timeout for receiving an ACK for that write. 276 * 277 * <p>If the timeout is reached before an ACK is received, the message write is retried. 278 */ 279 private final Runnable mSendMessageWithTimeoutRunnable = new Runnable() { 280 @Override 281 public void run() { 282 if (Log.isLoggable(TAG, Log.DEBUG)) { 283 Log.d(TAG, "Sending BLE message; retry count: " + mRetryCount); 284 } 285 286 if (mRetryCount < BLE_MESSAGE_RETRY_LIMIT) { 287 writeValueAndNotify(mMessageQueue.peek().toByteArray()); 288 mRetryCount++; 289 mHandler.postDelayed(this, BLE_MESSAGE_RETRY_DELAY_MS); 290 return; 291 } 292 293 mHandler.removeCallbacks(this); 294 mRetryCount = 0; 295 mMessageQueue.clear(); 296 297 Log.e(TAG, "Error during BLE message sending - exceeded retry limit."); 298 299 for (BleMessageStreamCallback callback : mCallbacks) { 300 callback.onWriteMessageError(); 301 } 302 } 303 }; 304 } 305