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