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