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.Manifest.permission.BLUETOOTH_CONNECT;
20 
21 import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
22 
23 import android.annotation.RequiresPermission;
24 import android.bluetooth.BluetoothDevice;
25 import android.bluetooth.BluetoothProfile;
26 import android.bluetooth.BluetoothUuid;
27 import android.bluetooth.IBluetoothBattery;
28 import android.content.AttributionSource;
29 import android.content.Context;
30 import android.os.Handler;
31 import android.os.HandlerThread;
32 import android.os.Looper;
33 import android.os.ParcelUuid;
34 import android.sysprop.BluetoothProperties;
35 import android.util.Log;
36 
37 import com.android.bluetooth.Utils;
38 import com.android.bluetooth.btservice.AdapterService;
39 import com.android.bluetooth.btservice.ProfileService;
40 import com.android.bluetooth.btservice.storage.DatabaseManager;
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import java.lang.ref.WeakReference;
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Objects;
50 
51 /** A profile service that connects to the Battery service (BAS) of BLE devices */
52 public class BatteryService extends ProfileService {
53     private static final String TAG = "BatteryService";
54 
55     // Timeout for state machine thread join, to prevent potential ANR.
56     private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1_000;
57 
58     private static final int MAX_BATTERY_STATE_MACHINES = 10;
59     private static BatteryService sBatteryService;
60     private AdapterService mAdapterService;
61     private DatabaseManager mDatabaseManager;
62     private HandlerThread mStateMachinesThread;
63     private Handler mHandler;
64     private final Map<BluetoothDevice, BatteryStateMachine> mStateMachines = new HashMap<>();
65 
BatteryService(Context ctx)66     public BatteryService(Context ctx) {
67         super(ctx);
68     }
69 
isEnabled()70     public static boolean isEnabled() {
71         return BluetoothProperties.isProfileBasClientEnabled().orElse(false);
72     }
73 
74     @Override
initBinder()75     protected IProfileServiceBinder initBinder() {
76         return new BluetoothBatteryBinder(this);
77     }
78 
79     @Override
start()80     public void start() {
81         Log.d(TAG, "start()");
82         if (sBatteryService != null) {
83             throw new IllegalStateException("start() called twice");
84         }
85 
86         mAdapterService =
87                 Objects.requireNonNull(
88                         AdapterService.getAdapterService(),
89                         "AdapterService cannot be null when BatteryService starts");
90         mDatabaseManager =
91                 Objects.requireNonNull(
92                         mAdapterService.getDatabase(),
93                         "DatabaseManager cannot be null when BatteryService starts");
94 
95         mHandler = new Handler(Looper.getMainLooper());
96         mStateMachines.clear();
97         mStateMachinesThread = new HandlerThread("BatteryService.StateMachines");
98         mStateMachinesThread.start();
99 
100         setBatteryService(this);
101     }
102 
103     @Override
stop()104     public void stop() {
105         Log.d(TAG, "stop()");
106         if (sBatteryService == null) {
107             Log.w(TAG, "stop() called before start()");
108             return;
109         }
110 
111         setBatteryService(null);
112 
113         // Destroy state machines and stop handler thread
114         synchronized (mStateMachines) {
115             for (BatteryStateMachine sm : mStateMachines.values()) {
116                 sm.doQuit();
117                 sm.cleanup();
118             }
119             mStateMachines.clear();
120         }
121 
122         if (mStateMachinesThread != null) {
123             try {
124                 mStateMachinesThread.quitSafely();
125                 mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
126                 mStateMachinesThread = null;
127             } catch (InterruptedException e) {
128                 // Do not rethrow as we are shutting down anyway
129             }
130         }
131 
132         // Unregister Handler and stop all queued messages.
133         if (mHandler != null) {
134             mHandler.removeCallbacksAndMessages(null);
135             mHandler = null;
136         }
137 
138         mAdapterService = null;
139     }
140 
141     @Override
cleanup()142     public void cleanup() {
143         Log.d(TAG, "cleanup()");
144     }
145 
146     /** Gets the BatteryService instance */
getBatteryService()147     public static synchronized BatteryService getBatteryService() {
148         if (sBatteryService == null) {
149             Log.w(TAG, "getBatteryService(): service is NULL");
150             return null;
151         }
152 
153         if (!sBatteryService.isAvailable()) {
154             Log.w(TAG, "getBatteryService(): service is not available");
155             return null;
156         }
157         return sBatteryService;
158     }
159 
160     /** Sets the battery service instance. It should be called only for testing purpose. */
161     @VisibleForTesting
setBatteryService(BatteryService instance)162     public static synchronized void setBatteryService(BatteryService instance) {
163         Log.d(TAG, "setBatteryService(): set to: " + instance);
164         sBatteryService = instance;
165     }
166 
167     /** Connects to the battery service of the given device. */
168     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
connect(BluetoothDevice device)169     public boolean connect(BluetoothDevice device) {
170         enforceCallingOrSelfPermission(
171                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
172         Log.d(TAG, "connect(): " + device);
173         if (device == null) {
174             Log.w(TAG, "Ignore connecting to null device");
175             return false;
176         }
177 
178         if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
179             Log.w(TAG, "Cannot connect to " + device + " : policy forbidden");
180             return false;
181         }
182         ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
183         if (!Utils.arrayContains(featureUuids, BluetoothUuid.BATTERY)) {
184             Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Battery UUID");
185             return false;
186         }
187 
188         synchronized (mStateMachines) {
189             BatteryStateMachine sm = getOrCreateStateMachine(device);
190             if (sm == null) {
191                 Log.e(TAG, "Cannot connect to " + device + " : no state machine");
192                 return false;
193             }
194             sm.sendMessage(BatteryStateMachine.CONNECT);
195         }
196 
197         return true;
198     }
199 
200     /**
201      * Connects to the battery service of the given device if possible. If it's impossible, it
202      * doesn't try without logging errors.
203      */
connectIfPossible(BluetoothDevice device)204     public boolean connectIfPossible(BluetoothDevice device) {
205         if (device == null
206                 || getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
207                 || !Utils.arrayContains(
208                         mAdapterService.getRemoteUuids(device), BluetoothUuid.BATTERY)) {
209             return false;
210         }
211         return connect(device);
212     }
213 
214     /** Disconnects from the battery service of the given device. */
215     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
disconnect(BluetoothDevice device)216     public boolean disconnect(BluetoothDevice device) {
217         enforceCallingOrSelfPermission(
218                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
219         Log.d(TAG, "disconnect(): " + device);
220         if (device == null) {
221             Log.w(TAG, "Ignore disconnecting to null device");
222             return false;
223         }
224         synchronized (mStateMachines) {
225             BatteryStateMachine sm = getOrCreateStateMachine(device);
226             if (sm != null) {
227                 sm.sendMessage(BatteryStateMachine.DISCONNECT);
228             }
229         }
230 
231         return true;
232     }
233 
234     /** Gets devices that battery service is connected. */
235     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectedDevices()236     public List<BluetoothDevice> getConnectedDevices() {
237         enforceCallingOrSelfPermission(
238                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
239         synchronized (mStateMachines) {
240             List<BluetoothDevice> devices = new ArrayList<>();
241             for (BatteryStateMachine sm : mStateMachines.values()) {
242                 if (sm.isConnected()) {
243                     devices.add(sm.getDevice());
244                 }
245             }
246             return devices;
247         }
248     }
249 
250     /**
251      * Check whether it can connect to a peer device. The check considers a number of factors during
252      * the evaluation.
253      *
254      * @param device the peer device to connect to
255      * @return true if connection is allowed, otherwise false
256      */
257     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
canConnect(BluetoothDevice device)258     public boolean canConnect(BluetoothDevice device) {
259         // Check connectionPolicy and accept or reject the connection.
260         int connectionPolicy = getConnectionPolicy(device);
261         int bondState = mAdapterService.getBondState(device);
262         // Allow this connection only if the device is bonded. Any attempt to connect while
263         // bonding would potentially lead to an unauthorized connection.
264         if (bondState != BluetoothDevice.BOND_BONDED) {
265             Log.w(TAG, "canConnect: return false, bondState=" + bondState);
266             return false;
267         } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN
268                 && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
269             // Otherwise, reject the connection if connectionPolicy is not valid.
270             Log.w(TAG, "canConnect: return false, connectionPolicy=" + connectionPolicy);
271             return false;
272         }
273         return true;
274     }
275 
276     /** Called when the connection state of a state machine is changed */
277     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
handleConnectionStateChanged(BatteryStateMachine sm, int fromState, int toState)278     public void handleConnectionStateChanged(BatteryStateMachine sm, int fromState, int toState) {
279         BluetoothDevice device = sm.getDevice();
280         if ((sm == null) || (fromState == toState)) {
281             Log.e(
282                     TAG,
283                     "connectionStateChanged: unexpected invocation. device="
284                             + device
285                             + " fromState="
286                             + fromState
287                             + " toState="
288                             + toState);
289             return;
290         }
291 
292         // Check if the device is disconnected - if unbonded, remove the state machine
293         if (toState == BluetoothProfile.STATE_DISCONNECTED) {
294             int bondState = mAdapterService.getBondState(device);
295             if (bondState == BluetoothDevice.BOND_NONE) {
296                 Log.d(TAG, device + " is unbonded. Remove state machine");
297                 removeStateMachine(device);
298             }
299         }
300     }
301 
302     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getDevicesMatchingConnectionStates(int[] states)303     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
304         enforceCallingOrSelfPermission(
305                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
306         ArrayList<BluetoothDevice> devices = new ArrayList<>();
307         if (states == null) {
308             return devices;
309         }
310         final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
311         if (bondedDevices == null) {
312             return devices;
313         }
314         synchronized (mStateMachines) {
315             for (BluetoothDevice device : bondedDevices) {
316                 int connectionState = BluetoothProfile.STATE_DISCONNECTED;
317                 BatteryStateMachine sm = mStateMachines.get(device);
318                 if (sm != null) {
319                     connectionState = sm.getConnectionState();
320                 }
321                 for (int state : states) {
322                     if (connectionState == state) {
323                         devices.add(device);
324                         break;
325                     }
326                 }
327             }
328             return devices;
329         }
330     }
331 
332     /**
333      * Get the list of devices that have state machines.
334      *
335      * @return the list of devices that have state machines
336      */
337     @VisibleForTesting
getDevices()338     List<BluetoothDevice> getDevices() {
339         List<BluetoothDevice> devices = new ArrayList<>();
340         synchronized (mStateMachines) {
341             for (BatteryStateMachine sm : mStateMachines.values()) {
342                 devices.add(sm.getDevice());
343             }
344             return devices;
345         }
346     }
347 
348     /** Gets the connection state of the given device's battery service */
349     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getConnectionState(BluetoothDevice device)350     public int getConnectionState(BluetoothDevice device) {
351         enforceCallingOrSelfPermission(BLUETOOTH_CONNECT, "Need BLUETOOTH_CONNECT permission");
352         synchronized (mStateMachines) {
353             BatteryStateMachine sm = mStateMachines.get(device);
354             if (sm == null) {
355                 return BluetoothProfile.STATE_DISCONNECTED;
356             }
357             return sm.getConnectionState();
358         }
359     }
360 
361     /**
362      * Set connection policy of the profile and connects it if connectionPolicy is {@link
363      * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link
364      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
365      *
366      * <p>The device should already be paired. Connection policy can be one of: {@link
367      * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link
368      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
369      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
370      *
371      * @param device the remote device
372      * @param connectionPolicy is the connection policy to set to for this profile
373      * @return true on success, otherwise false
374      */
375     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)376     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
377         enforceCallingOrSelfPermission(
378                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
379         Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
380         mDatabaseManager.setProfileConnectionPolicy(
381                 device, BluetoothProfile.BATTERY, connectionPolicy);
382         if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
383             connect(device);
384         } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
385             disconnect(device);
386         }
387         return true;
388     }
389 
390     /** Gets the connection policy for the battery service of the given device. */
391     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(BluetoothDevice device)392     public int getConnectionPolicy(BluetoothDevice device) {
393         enforceCallingOrSelfPermission(
394                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
395         return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.BATTERY);
396     }
397 
398     /** Called when the battery level of the device is notified. */
399     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
handleBatteryChanged(BluetoothDevice device, int batteryLevel)400     public void handleBatteryChanged(BluetoothDevice device, int batteryLevel) {
401         mAdapterService.setBatteryLevel(device, batteryLevel, /* isBas= */ true);
402     }
403 
getOrCreateStateMachine(BluetoothDevice device)404     private BatteryStateMachine getOrCreateStateMachine(BluetoothDevice device) {
405         if (device == null) {
406             Log.e(TAG, "getOrCreateGatt failed: device cannot be null");
407             return null;
408         }
409         synchronized (mStateMachines) {
410             BatteryStateMachine sm = mStateMachines.get(device);
411             if (sm != null) {
412                 return sm;
413             }
414             // Limit the maximum number of state machines to avoid DoS attack
415             if (mStateMachines.size() >= MAX_BATTERY_STATE_MACHINES) {
416                 Log.e(
417                         TAG,
418                         "Maximum number of Battery state machines reached: "
419                                 + MAX_BATTERY_STATE_MACHINES);
420                 return null;
421             }
422             Log.d(TAG, "Creating a new state machine for " + device);
423             sm = BatteryStateMachine.make(device, this, mStateMachinesThread.getLooper());
424             mStateMachines.put(device, sm);
425             return sm;
426         }
427     }
428 
429     /** Process a change in the bonding state for a device */
handleBondStateChanged(BluetoothDevice device, int fromState, int toState)430     public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) {
431         mHandler.post(() -> bondStateChanged(device, toState));
432     }
433 
434     /**
435      * Remove state machine if the bonding for a device is removed
436      *
437      * @param device the device whose bonding state has changed
438      * @param bondState the new bond state for the device. Possible values are: {@link
439      *     BluetoothDevice#BOND_NONE}, {@link BluetoothDevice#BOND_BONDING}, {@link
440      *     BluetoothDevice#BOND_BONDED}, {@link BluetoothDevice#ERROR}.
441      */
442     @VisibleForTesting
bondStateChanged(BluetoothDevice device, int bondState)443     void bondStateChanged(BluetoothDevice device, int bondState) {
444         Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
445         // Remove state machine if the bonding for a device is removed
446         if (bondState != BluetoothDevice.BOND_NONE) {
447             return;
448         }
449 
450         synchronized (mStateMachines) {
451             BatteryStateMachine sm = mStateMachines.get(device);
452             if (sm == null) {
453                 return;
454             }
455             if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
456                 return;
457             }
458             removeStateMachine(device);
459         }
460     }
461 
removeStateMachine(BluetoothDevice device)462     private void removeStateMachine(BluetoothDevice device) {
463         if (device == null) {
464             Log.e(TAG, "removeStateMachine failed: device cannot be null");
465             return;
466         }
467         synchronized (mStateMachines) {
468             BatteryStateMachine sm = mStateMachines.remove(device);
469             if (sm == null) {
470                 Log.w(
471                         TAG,
472                         "removeStateMachine: device " + device + " does not have a state machine");
473                 return;
474             }
475             Log.i(TAG, "removeGatt: removing bluetooth gatt for device: " + device);
476             sm.doQuit();
477             sm.cleanup();
478         }
479     }
480 
481     /** Binder object: must be a static class or memory leak may occur */
482     @VisibleForTesting
483     static class BluetoothBatteryBinder extends IBluetoothBattery.Stub
484             implements IProfileServiceBinder {
485         private final WeakReference<BatteryService> mServiceRef;
486 
487         @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getService(AttributionSource source)488         private BatteryService getService(AttributionSource source) {
489             BatteryService service = mServiceRef.get();
490             if (Utils.isInstrumentationTestMode()) {
491                 return service;
492             }
493 
494             if (!Utils.checkServiceAvailable(service, TAG)
495                     || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)
496                     || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
497                 return null;
498             }
499             return service;
500         }
501 
BluetoothBatteryBinder(BatteryService svc)502         BluetoothBatteryBinder(BatteryService svc) {
503             mServiceRef = new WeakReference<>(svc);
504         }
505 
506         @Override
cleanup()507         public void cleanup() {
508             mServiceRef.clear();
509         }
510 
511         @Override
connect(BluetoothDevice device, AttributionSource source)512         public boolean connect(BluetoothDevice device, AttributionSource source) {
513             BatteryService service = getService(source);
514             if (service == null) {
515                 return false;
516             }
517 
518             return service.connect(device);
519         }
520 
521         @Override
disconnect(BluetoothDevice device, AttributionSource source)522         public boolean disconnect(BluetoothDevice device, AttributionSource source) {
523             BatteryService service = getService(source);
524             if (service == null) {
525                 return false;
526             }
527 
528             return service.disconnect(device);
529         }
530 
531         @Override
getConnectedDevices(AttributionSource source)532         public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
533             BatteryService service = getService(source);
534             if (service == null) {
535                 return Collections.emptyList();
536             }
537 
538             enforceBluetoothPrivilegedPermission(service);
539             return service.getConnectedDevices();
540         }
541 
542         @Override
getDevicesMatchingConnectionStates( int[] states, AttributionSource source)543         public List<BluetoothDevice> getDevicesMatchingConnectionStates(
544                 int[] states, AttributionSource source) {
545             BatteryService service = getService(source);
546             if (service == null) {
547                 return Collections.emptyList();
548             }
549 
550             return service.getDevicesMatchingConnectionStates(states);
551         }
552 
553         @Override
getConnectionState(BluetoothDevice device, AttributionSource source)554         public int getConnectionState(BluetoothDevice device, AttributionSource source) {
555             BatteryService service = getService(source);
556             if (service == null) {
557                 return BluetoothProfile.STATE_DISCONNECTED;
558             }
559 
560             return service.getConnectionState(device);
561         }
562 
563         @Override
setConnectionPolicy( BluetoothDevice device, int connectionPolicy, AttributionSource source)564         public boolean setConnectionPolicy(
565                 BluetoothDevice device, int connectionPolicy, AttributionSource source) {
566             BatteryService service = getService(source);
567             if (service == null) {
568                 return false;
569             }
570 
571             return service.setConnectionPolicy(device, connectionPolicy);
572         }
573 
574         @Override
getConnectionPolicy(BluetoothDevice device, AttributionSource source)575         public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) {
576             BatteryService service = getService(source);
577             if (service == null) {
578                 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
579             }
580 
581             return service.getConnectionPolicy(device);
582         }
583     }
584 
585     @Override
dump(StringBuilder sb)586     public void dump(StringBuilder sb) {
587         super.dump(sb);
588         for (BatteryStateMachine sm : mStateMachines.values()) {
589             sm.dump(sb);
590         }
591     }
592 }
593