• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.bluetooth.bas;
18 
19 import static android.bluetooth.BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
20 import static android.bluetooth.BluetoothDevice.PHY_LE_1M_MASK;
21 import static android.bluetooth.BluetoothDevice.PHY_LE_2M_MASK;
22 import static android.bluetooth.BluetoothDevice.TRANSPORT_LE;
23 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
24 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
25 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
26 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING;
27 import static android.bluetooth.BluetoothProfile.getConnectionStateName;
28 
29 import static java.util.Objects.requireNonNull;
30 
31 import android.annotation.SuppressLint;
32 import android.bluetooth.BluetoothDevice;
33 import android.bluetooth.BluetoothGatt;
34 import android.bluetooth.BluetoothGattCallback;
35 import android.bluetooth.BluetoothGattCharacteristic;
36 import android.bluetooth.BluetoothGattDescriptor;
37 import android.bluetooth.BluetoothGattService;
38 import android.bluetooth.BluetoothProfile;
39 import android.content.AttributionSource;
40 import android.os.Looper;
41 import android.os.Message;
42 import android.util.Log;
43 
44 import com.android.bluetooth.btservice.ProfileService;
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.internal.util.State;
47 import com.android.internal.util.StateMachine;
48 
49 import java.io.FileDescriptor;
50 import java.io.PrintWriter;
51 import java.io.StringWriter;
52 import java.time.Duration;
53 import java.util.Scanner;
54 import java.util.UUID;
55 
56 /** It manages Battery service of a BLE device */
57 public class BatteryStateMachine extends StateMachine {
58     private static final String TAG = BatteryStateMachine.class.getSimpleName();
59 
60     static final UUID GATT_BATTERY_SERVICE_UUID =
61             UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
62     static final UUID GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID =
63             UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
64     static final UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID =
65             UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
66 
67     static final int MESSAGE_CONNECT = 1;
68     static final int MESSAGE_DISCONNECT = 2;
69     @VisibleForTesting static final int MESSAGE_CONNECTION_STATE_CHANGED = 3;
70     private static final int MESSAGE_CONNECT_TIMEOUT = 201;
71 
72     @VisibleForTesting static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(30);
73 
74     private final Disconnected mDisconnected;
75     private final Connecting mConnecting;
76     private final Connected mConnected;
77     private final Disconnecting mDisconnecting;
78     private int mLastConnectionState = STATE_DISCONNECTED;
79 
80     private final BatteryService mService;
81 
82     BluetoothGatt mBluetoothGatt;
83     private final GattCallback mGattCallback = new GattCallback();
84     final BluetoothDevice mDevice;
85 
BatteryStateMachine(BatteryService service, BluetoothDevice device, Looper looper)86     BatteryStateMachine(BatteryService service, BluetoothDevice device, Looper looper) {
87         super(TAG, looper);
88         mService = requireNonNull(service);
89         mDevice = device;
90 
91         mDisconnected = new Disconnected();
92         mConnecting = new Connecting();
93         mConnected = new Connected();
94         mDisconnecting = new Disconnecting();
95 
96         addState(mDisconnected);
97         addState(mConnecting);
98         addState(mDisconnecting);
99         addState(mConnected);
100 
101         setInitialState(mDisconnected);
102         start();
103     }
104 
105     /** Quits the state machine */
doQuit()106     public void doQuit() {
107         log("doQuit for device " + mDevice);
108         quitNow();
109     }
110 
111     /** Cleans up the resources the state machine held. */
112     @SuppressLint("AndroidFrameworkRequiresPermission") // We should call internal gatt interface
cleanup()113     public void cleanup() {
114         log("cleanup for device " + mDevice);
115         if (mBluetoothGatt != null) {
116             mBluetoothGatt.close();
117             mBluetoothGatt = null;
118         }
119     }
120 
getDevice()121     BluetoothDevice getDevice() {
122         return mDevice;
123     }
124 
isConnected()125     synchronized boolean isConnected() {
126         return mLastConnectionState == STATE_CONNECTED;
127     }
128 
messageWhatToString(int what)129     private static String messageWhatToString(int what) {
130         return switch (what) {
131             case MESSAGE_CONNECT -> "CONNECT";
132             case MESSAGE_DISCONNECT -> "DISCONNECT";
133             case MESSAGE_CONNECTION_STATE_CHANGED -> "CONNECTION_STATE_CHANGED";
134             case MESSAGE_CONNECT_TIMEOUT -> "CONNECT_TIMEOUT";
135             default -> Integer.toString(what);
136         };
137     }
138 
139     /** Dumps battery state machine state. */
dump(StringBuilder sb)140     public void dump(StringBuilder sb) {
141         ProfileService.println(sb, "mDevice: " + mDevice);
142         ProfileService.println(sb, "  StateMachine: " + this);
143         ProfileService.println(sb, "  BluetoothGatt: " + mBluetoothGatt);
144         // Dump the state machine logs
145         StringWriter stringWriter = new StringWriter();
146         PrintWriter printWriter = new PrintWriter(stringWriter);
147         super.dump(new FileDescriptor(), printWriter, new String[] {});
148         printWriter.flush();
149         stringWriter.flush();
150         ProfileService.println(sb, "  StateMachineLog:");
151         Scanner scanner = new Scanner(stringWriter.toString());
152         while (scanner.hasNextLine()) {
153             String line = scanner.nextLine();
154             ProfileService.println(sb, "    " + line);
155         }
156         scanner.close();
157     }
158 
159     @BluetoothProfile.BtProfileState
getConnectionState()160     int getConnectionState() {
161         return mLastConnectionState;
162     }
163 
dispatchConnectionStateChanged(int toState)164     void dispatchConnectionStateChanged(int toState) {
165         log(
166                 "Connection state change "
167                         + mDevice
168                         + ": "
169                         + getConnectionStateName(mLastConnectionState)
170                         + "->"
171                         + getConnectionStateName(toState));
172 
173         mService.handleConnectionStateChanged(mDevice, mLastConnectionState, toState);
174     }
175 
176     // Allow test to abstract the unmockable mBluetoothGatt
177     @VisibleForTesting
178     @SuppressLint("AndroidFrameworkRequiresPermission") // We should call internal gatt interface
connectGatt()179     boolean connectGatt() {
180         mDevice.setAttributionSource(
181                 (new AttributionSource.Builder(AttributionSource.myAttributionSource()))
182                         .setAttributionTag("BatteryService")
183                         .build());
184         mBluetoothGatt =
185                 mDevice.connectGatt(
186                         mService,
187                         /* autoConnect= */ false,
188                         mGattCallback,
189                         TRANSPORT_LE,
190                         /* opportunistic= */ true,
191                         PHY_LE_1M_MASK | PHY_LE_2M_MASK,
192                         getHandler());
193         return mBluetoothGatt != null;
194     }
195 
196     // Allow test to abstract the unmockable BluetoothGatt
197     @VisibleForTesting
198     @SuppressLint("AndroidFrameworkRequiresPermission") // We should call internal gatt interface
disconnectGatt()199     void disconnectGatt() {
200         mBluetoothGatt.disconnect();
201     }
202 
203     // Allow test to abstract the unmockable BluetoothGatt
204     @VisibleForTesting
205     @SuppressLint("AndroidFrameworkRequiresPermission") // We should call internal gatt interface
discoverServicesGatt()206     void discoverServicesGatt() {
207         mBluetoothGatt.discoverServices();
208     }
209 
210     @VisibleForTesting
updateBatteryLevel(byte[] value)211     void updateBatteryLevel(byte[] value) {
212         if (value.length == 0) {
213             return;
214         }
215         int batteryLevel = value[0] & 0xFF;
216 
217         mService.handleBatteryChanged(mDevice, batteryLevel);
218     }
219 
220     @VisibleForTesting
resetBatteryLevel()221     void resetBatteryLevel() {
222         mService.handleBatteryChanged(mDevice, BATTERY_LEVEL_UNKNOWN);
223     }
224 
log(String tag, String msg)225     static void log(String tag, String msg) {
226         Log.d(tag, msg);
227     }
228 
229     @VisibleForTesting
230     class Disconnected extends State {
231         private static final String TAG =
232                 BatteryStateMachine.TAG + "." + Disconnected.class.getSimpleName();
233 
234         @Override
enter()235         public void enter() {
236             log(TAG, "Enter (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
237 
238             if (mBluetoothGatt != null) {
239                 mBluetoothGatt.close();
240                 mBluetoothGatt = null;
241             }
242 
243             if (mLastConnectionState != STATE_DISCONNECTED) {
244                 // Don't broadcast during startup
245                 dispatchConnectionStateChanged(STATE_DISCONNECTED);
246             }
247             mLastConnectionState = STATE_DISCONNECTED;
248         }
249 
250         @Override
exit()251         public void exit() {
252             log(TAG, "Exit (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
253         }
254 
255         @Override
processMessage(Message message)256         public boolean processMessage(Message message) {
257             log(TAG, "Process message(" + mDevice + "): " + messageWhatToString(message.what));
258 
259             switch (message.what) {
260                 case MESSAGE_CONNECT -> {
261                     log(TAG, "Connecting to " + mDevice);
262                     if (!mService.canConnect(mDevice)) {
263                         Log.w(TAG, "Battery connecting request rejected: " + mDevice);
264                     } else {
265                         if (connectGatt()) {
266                             transitionTo(mConnecting);
267                         } else {
268                             Log.w(
269                                     TAG,
270                                     "Battery connecting request rejected due to "
271                                             + "GATT connection rejection: "
272                                             + mDevice);
273                         }
274                     }
275                 }
276                 default -> {
277                     Log.e(TAG, "Unexpected message: " + messageWhatToString(message.what));
278                     return NOT_HANDLED;
279                 }
280             }
281             return HANDLED;
282         }
283     }
284 
285     @VisibleForTesting
286     class Connecting extends State {
287         private static final String TAG =
288                 BatteryStateMachine.TAG + "." + Connecting.class.getSimpleName();
289 
290         @Override
enter()291         public void enter() {
292             log(TAG, "Enter (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
293             dispatchConnectionStateChanged(STATE_CONNECTING);
294             mLastConnectionState = STATE_CONNECTING;
295         }
296 
297         @Override
exit()298         public void exit() {
299             log(TAG, "Exit (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
300         }
301 
302         @Override
processMessage(Message message)303         public boolean processMessage(Message message) {
304             log(TAG, "process message(" + mDevice + "): " + messageWhatToString(message.what));
305 
306             switch (message.what) {
307                 case MESSAGE_DISCONNECT -> {
308                     log(TAG, "Connection canceled to " + mDevice);
309                     disconnectGatt();
310                     // As we're not yet connected we don't need to wait for callbacks.
311                     transitionTo(mDisconnected);
312                 }
313                 case MESSAGE_CONNECTION_STATE_CHANGED -> processConnectionEvent(message.arg1);
314                 default -> {
315                     Log.e(TAG, "Unexpected message: " + messageWhatToString(message.what));
316                     return NOT_HANDLED;
317                 }
318             }
319             return HANDLED;
320         }
321 
322         // in Connecting state
processConnectionEvent(int state)323         private void processConnectionEvent(int state) {
324             switch (state) {
325                 case STATE_DISCONNECTED -> {
326                     Log.w(TAG, "Device disconnected: " + mDevice);
327                     transitionTo(mDisconnected);
328                 }
329                 case STATE_CONNECTED -> transitionTo(mConnected);
330                 default -> Log.e(TAG, "Incorrect state: " + state);
331             }
332         }
333     }
334 
335     @VisibleForTesting
336     class Disconnecting extends State {
337         private static final String TAG =
338                 BatteryStateMachine.TAG + "." + Disconnecting.class.getSimpleName();
339 
340         @Override
enter()341         public void enter() {
342             log(TAG, "Enter (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
343             sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT.toMillis());
344             dispatchConnectionStateChanged(STATE_DISCONNECTING);
345             mLastConnectionState = STATE_DISCONNECTING;
346         }
347 
348         @Override
exit()349         public void exit() {
350             log(TAG, "Exit (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
351             removeMessages(MESSAGE_CONNECT_TIMEOUT);
352         }
353 
354         @Override
processMessage(Message message)355         public boolean processMessage(Message message) {
356             log(TAG, "Process message(" + mDevice + "): " + messageWhatToString(message.what));
357 
358             switch (message.what) {
359                 case MESSAGE_CONNECT_TIMEOUT -> {
360                     Log.w(TAG, "Disconnection timeout: " + mDevice);
361                     transitionTo(mDisconnected);
362                 }
363                 case MESSAGE_CONNECTION_STATE_CHANGED -> {
364                     processConnectionEvent(message.arg1);
365                 }
366                 default -> {
367                     Log.e(TAG, "Unexpected message: " + messageWhatToString(message.what));
368                     return NOT_HANDLED;
369                 }
370             }
371             return HANDLED;
372         }
373 
374         // in Disconnecting state
processConnectionEvent(int state)375         private void processConnectionEvent(int state) {
376             switch (state) {
377                 case STATE_DISCONNECTED -> {
378                     Log.i(TAG, "Disconnected: " + mDevice);
379                     transitionTo(mDisconnected);
380                 }
381                 case STATE_CONNECTED -> {
382                     // TODO: Check if connect while disconnecting is okay. It is related to
383                     // MESSAGE_CONNECT_TIMEOUT as well.
384 
385                     // Reject the connection and stay in Disconnecting state
386                     Log.w(TAG, "Incoming Battery connected request rejected: " + mDevice);
387                     disconnectGatt();
388                 }
389                 default -> {
390                     Log.e(TAG, "Incorrect state: " + state);
391                 }
392             }
393         }
394     }
395 
396     @VisibleForTesting
397     class Connected extends State {
398         private static final String TAG =
399                 BatteryStateMachine.TAG + "." + Connected.class.getSimpleName();
400 
401         @Override
enter()402         public void enter() {
403             log(TAG, "Enter (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
404             dispatchConnectionStateChanged(STATE_CONNECTED);
405             mLastConnectionState = STATE_CONNECTED;
406 
407             discoverServicesGatt();
408         }
409 
410         @Override
exit()411         public void exit() {
412             log(TAG, "Exit (" + mDevice + "): " + messageWhatToString(getCurrentMessage().what));
413             // Reset the battery level only after connected
414             resetBatteryLevel();
415         }
416 
417         @Override
processMessage(Message message)418         public boolean processMessage(Message message) {
419             log(TAG, "Process message(" + mDevice + "): " + messageWhatToString(message.what));
420 
421             switch (message.what) {
422                 case MESSAGE_DISCONNECT -> {
423                     log(TAG, "Disconnecting from " + mDevice);
424                     disconnectGatt();
425                     transitionTo(mDisconnecting);
426                 }
427                 case MESSAGE_CONNECTION_STATE_CHANGED -> {
428                     processConnectionEvent(message.arg1);
429                 }
430                 default -> {
431                     Log.e(TAG, "Unexpected message: " + messageWhatToString(message.what));
432                     return NOT_HANDLED;
433                 }
434             }
435             return HANDLED;
436         }
437 
438         // in Connected state
processConnectionEvent(int state)439         private void processConnectionEvent(int state) {
440             switch (state) {
441                 case STATE_DISCONNECTED -> {
442                     Log.i(TAG, "Disconnected from " + mDevice);
443                     transitionTo(mDisconnected);
444                 }
445                 default -> {
446                     Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state);
447                 }
448             }
449         }
450     }
451 
452     final class GattCallback extends BluetoothGattCallback {
453         @Override
onConnectionStateChange(BluetoothGatt gatt, int status, int newState)454         public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
455             sendMessage(MESSAGE_CONNECTION_STATE_CHANGED, newState);
456         }
457 
458         @Override
459         @SuppressLint("AndroidFrameworkRequiresPermission") // We should call internal gatt itf
onServicesDiscovered(BluetoothGatt gatt, int status)460         public void onServicesDiscovered(BluetoothGatt gatt, int status) {
461             if (status != BluetoothGatt.GATT_SUCCESS) {
462                 Log.e(TAG, "No gatt service");
463                 return;
464             }
465 
466             final BluetoothGattService batteryService = gatt.getService(GATT_BATTERY_SERVICE_UUID);
467             if (batteryService == null) {
468                 Log.e(TAG, "No battery service");
469                 return;
470             }
471 
472             final BluetoothGattCharacteristic batteryLevel =
473                     batteryService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID);
474             if (batteryLevel == null) {
475                 Log.e(TAG, "No battery level characteristic");
476                 return;
477             }
478 
479             // This may not trigger onCharacteristicRead if CCCD is already set but then
480             // onCharacteristicChanged will be triggered soon.
481             gatt.readCharacteristic(batteryLevel);
482         }
483 
484         @Override
onCharacteristicChanged( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value)485         public void onCharacteristicChanged(
486                 BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
487             if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
488                 updateBatteryLevel(value);
489             }
490         }
491 
492         @Override
493         @SuppressLint("AndroidFrameworkRequiresPermission") // We should call internal gatt itf
onCharacteristicRead( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, int status)494         public void onCharacteristicRead(
495                 BluetoothGatt gatt,
496                 BluetoothGattCharacteristic characteristic,
497                 byte[] value,
498                 int status) {
499             if (status != BluetoothGatt.GATT_SUCCESS) {
500                 Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic);
501                 return;
502             }
503 
504             if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
505                 updateBatteryLevel(value);
506                 BluetoothGattDescriptor cccd =
507                         characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
508                 if (cccd != null) {
509                     gatt.setCharacteristicNotification(characteristic, /* enable= */ true);
510                     gatt.writeDescriptor(cccd, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
511                 } else {
512                     Log.w(
513                             TAG,
514                             "No CCCD for battery level characteristic, " + "it won't be notified");
515                 }
516             }
517         }
518 
519         @Override
onDescriptorWrite( BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)520         public void onDescriptorWrite(
521                 BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
522             if (status != BluetoothGatt.GATT_SUCCESS) {
523                 Log.w(TAG, "Failed to write descriptor " + descriptor.getUuid());
524             }
525         }
526     }
527 }
528