• 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.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
22 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
23 
24 import static java.util.Objects.requireNonNull;
25 
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothProfile;
28 import android.bluetooth.BluetoothUuid;
29 import android.os.Handler;
30 import android.os.HandlerThread;
31 import android.os.Looper;
32 import android.os.ParcelUuid;
33 import android.sysprop.BluetoothProperties;
34 import android.util.Log;
35 
36 import com.android.bluetooth.Utils;
37 import com.android.bluetooth.btservice.AdapterService;
38 import com.android.bluetooth.btservice.ProfileService;
39 import com.android.bluetooth.btservice.storage.DatabaseManager;
40 import com.android.internal.annotations.GuardedBy;
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 
48 /** A profile service that connects to the Battery service (BAS) of BLE devices */
49 public class BatteryService extends ProfileService {
50     private static final String TAG = BatteryService.class.getSimpleName();
51 
52     // Timeout for state machine thread join, to prevent potential ANR.
53     private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1_000;
54 
55     private static BatteryService sBatteryService;
56 
57     private final AdapterService mAdapterService;
58     private final DatabaseManager mDatabaseManager;
59     private final HandlerThread mStateMachinesThread;
60     private final Handler mHandler;
61 
62     @GuardedBy("mStateMachines")
63     private final Map<BluetoothDevice, BatteryStateMachine> mStateMachines = new HashMap<>();
64 
BatteryService(AdapterService adapterService)65     public BatteryService(AdapterService adapterService) {
66         this(adapterService, Looper.getMainLooper());
67     }
68 
69     @VisibleForTesting
BatteryService(AdapterService adapterService, Looper looper)70     BatteryService(AdapterService adapterService, Looper looper) {
71         super(requireNonNull(adapterService));
72         mAdapterService = adapterService;
73         mDatabaseManager = requireNonNull(mAdapterService.getDatabase());
74         mHandler = new Handler(requireNonNull(looper));
75 
76         mStateMachinesThread = new HandlerThread("BatteryService.StateMachines");
77         mStateMachinesThread.start();
78         setBatteryService(this);
79     }
80 
isEnabled()81     public static boolean isEnabled() {
82         return BluetoothProperties.isProfileBasClientEnabled().orElse(false);
83     }
84 
85     @Override
initBinder()86     protected IProfileServiceBinder initBinder() {
87         return null;
88     }
89 
90     @Override
cleanup()91     public void cleanup() {
92         Log.i(TAG, "Cleanup Battery Service");
93 
94         setBatteryService(null);
95 
96         // Destroy state machines and stop handler thread
97         synchronized (mStateMachines) {
98             for (BatteryStateMachine sm : mStateMachines.values()) {
99                 sm.doQuit();
100                 sm.cleanup();
101             }
102             mStateMachines.clear();
103         }
104 
105         try {
106             mStateMachinesThread.quitSafely();
107             mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
108         } catch (InterruptedException e) {
109             // Do not rethrow as we are shutting down anyway
110         }
111 
112         mHandler.removeCallbacksAndMessages(null);
113     }
114 
115     /** Gets the BatteryService instance */
getBatteryService()116     public static synchronized BatteryService getBatteryService() {
117         if (sBatteryService == null) {
118             Log.w(TAG, "getBatteryService(): service is NULL");
119             return null;
120         }
121 
122         if (!sBatteryService.isAvailable()) {
123             Log.w(TAG, "getBatteryService(): service is not available");
124             return null;
125         }
126         return sBatteryService;
127     }
128 
129     /** Sets the battery service instance. It should be called only for testing purpose. */
130     @VisibleForTesting
setBatteryService(BatteryService instance)131     public static synchronized void setBatteryService(BatteryService instance) {
132         Log.d(TAG, "setBatteryService(): set to: " + instance);
133         sBatteryService = instance;
134     }
135 
136     /** Connects to the battery service of the given device. */
connect(BluetoothDevice device)137     public boolean connect(BluetoothDevice device) {
138         Log.d(TAG, "connect(): " + device);
139         if (device == null) {
140             Log.w(TAG, "Ignore connecting to null device");
141             return false;
142         }
143 
144         if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) {
145             Log.w(TAG, "Cannot connect to " + device + " : policy forbidden");
146             return false;
147         }
148         final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
149         if (!Utils.arrayContains(featureUuids, BluetoothUuid.BATTERY)) {
150             Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Battery UUID");
151             return false;
152         }
153 
154         synchronized (mStateMachines) {
155             BatteryStateMachine sm = getOrCreateStateMachine(device);
156             if (sm == null) {
157                 Log.e(TAG, "Cannot connect to " + device + " : no state machine");
158                 return false;
159             }
160             sm.sendMessage(BatteryStateMachine.MESSAGE_CONNECT);
161         }
162 
163         return true;
164     }
165 
166     /**
167      * Connects to the battery service of the given device if possible. If it's impossible, it
168      * doesn't try without logging errors.
169      */
connectIfPossible(BluetoothDevice device)170     public boolean connectIfPossible(BluetoothDevice device) {
171         if (device == null
172                 || getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN
173                 || !Utils.arrayContains(
174                         mAdapterService.getRemoteUuids(device), BluetoothUuid.BATTERY)) {
175             return false;
176         }
177         return connect(device);
178     }
179 
180     /** Disconnects from the battery service of the given device. */
disconnect(BluetoothDevice device)181     public boolean disconnect(BluetoothDevice device) {
182         Log.d(TAG, "disconnect(): " + device);
183         if (device == null) {
184             Log.w(TAG, "Ignore disconnecting to null device");
185             return false;
186         }
187         synchronized (mStateMachines) {
188             BatteryStateMachine sm = getOrCreateStateMachine(device);
189             if (sm != null) {
190                 sm.sendMessage(BatteryStateMachine.MESSAGE_DISCONNECT);
191             }
192         }
193 
194         return true;
195     }
196 
197     /** Gets devices that battery service is connected. */
getConnectedDevices()198     public List<BluetoothDevice> getConnectedDevices() {
199         synchronized (mStateMachines) {
200             List<BluetoothDevice> devices = new ArrayList<>();
201             for (BatteryStateMachine sm : mStateMachines.values()) {
202                 if (sm.isConnected()) {
203                     devices.add(sm.getDevice());
204                 }
205             }
206             return devices;
207         }
208     }
209 
210     /**
211      * Check whether it can connect to a peer device. The check considers a number of factors during
212      * the evaluation.
213      */
canConnect(BluetoothDevice device)214     boolean canConnect(BluetoothDevice device) {
215         // Check connectionPolicy and accept or reject the connection.
216         int connectionPolicy = getConnectionPolicy(device);
217         int bondState = mAdapterService.getBondState(device);
218         // Allow this connection only if the device is bonded. Any attempt to connect while
219         // bonding would potentially lead to an unauthorized connection.
220         if (bondState != BluetoothDevice.BOND_BONDED) {
221             Log.w(TAG, "canConnect: return false, bondState=" + bondState);
222             return false;
223         } else if (connectionPolicy != CONNECTION_POLICY_UNKNOWN
224                 && connectionPolicy != CONNECTION_POLICY_ALLOWED) {
225             // Otherwise, reject the connection if connectionPolicy is not valid.
226             Log.w(TAG, "canConnect: return false, connectionPolicy=" + connectionPolicy);
227             return false;
228         }
229         return true;
230     }
231 
232     /** Called when the connection state of a state machine is changed */
handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState)233     void handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState) {
234         if (fromState == toState) {
235             Log.e(
236                     TAG,
237                     "connectionStateChanged: unexpected invocation. device="
238                             + device
239                             + " fromState="
240                             + fromState
241                             + " toState="
242                             + toState);
243             return;
244         }
245 
246         // Check if the device is disconnected - if unbonded, remove the state machine
247         if (toState == STATE_DISCONNECTED) {
248             int bondState = mAdapterService.getBondState(device);
249             if (bondState == BluetoothDevice.BOND_NONE) {
250                 Log.d(TAG, device + " is unbonded. Remove state machine");
251                 removeStateMachine(device);
252             }
253         }
254     }
255 
getDevicesMatchingConnectionStates(int[] states)256     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
257         ArrayList<BluetoothDevice> devices = new ArrayList<>();
258         if (states == null) {
259             return devices;
260         }
261         final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
262         if (bondedDevices == null) {
263             return devices;
264         }
265         synchronized (mStateMachines) {
266             for (BluetoothDevice device : bondedDevices) {
267                 int connectionState = STATE_DISCONNECTED;
268                 BatteryStateMachine sm = mStateMachines.get(device);
269                 if (sm != null) {
270                     connectionState = sm.getConnectionState();
271                 }
272                 for (int state : states) {
273                     if (connectionState == state) {
274                         devices.add(device);
275                         break;
276                     }
277                 }
278             }
279             return devices;
280         }
281     }
282 
283     /**
284      * Get the list of devices that have state machines.
285      *
286      * @return the list of devices that have state machines
287      */
288     @VisibleForTesting
getDevices()289     List<BluetoothDevice> getDevices() {
290         List<BluetoothDevice> devices = new ArrayList<>();
291         synchronized (mStateMachines) {
292             for (BatteryStateMachine sm : mStateMachines.values()) {
293                 devices.add(sm.getDevice());
294             }
295             return devices;
296         }
297     }
298 
299     /** Gets the connection state of the given device's battery service */
getConnectionState(BluetoothDevice device)300     public int getConnectionState(BluetoothDevice device) {
301         synchronized (mStateMachines) {
302             BatteryStateMachine sm = mStateMachines.get(device);
303             if (sm == null) {
304                 return STATE_DISCONNECTED;
305             }
306             return sm.getConnectionState();
307         }
308     }
309 
310     /**
311      * Set connection policy of the profile and connects it if connectionPolicy is {@link
312      * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link
313      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
314      *
315      * <p>The device should already be paired. Connection policy can be one of: {@link
316      * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link
317      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
318      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
319      *
320      * @param device the remote device
321      * @param connectionPolicy is the connection policy to set to for this profile
322      * @return true on success, otherwise false
323      */
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)324     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
325         Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
326         mDatabaseManager.setProfileConnectionPolicy(
327                 device, BluetoothProfile.BATTERY, connectionPolicy);
328         if (connectionPolicy == CONNECTION_POLICY_ALLOWED) {
329             connect(device);
330         } else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) {
331             disconnect(device);
332         }
333         return true;
334     }
335 
336     /** Gets the connection policy for the battery service of the given device. */
getConnectionPolicy(BluetoothDevice device)337     public int getConnectionPolicy(BluetoothDevice device) {
338         return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.BATTERY);
339     }
340 
341     /** Called when the battery level of the device is notified. */
handleBatteryChanged(BluetoothDevice device, int batteryLevel)342     void handleBatteryChanged(BluetoothDevice device, int batteryLevel) {
343         mAdapterService.setBatteryLevel(device, batteryLevel, /* isBas= */ true);
344     }
345 
getOrCreateStateMachine(BluetoothDevice device)346     private BatteryStateMachine getOrCreateStateMachine(BluetoothDevice device) {
347         synchronized (mStateMachines) {
348             BatteryStateMachine sm = mStateMachines.get(device);
349             if (sm != null) {
350                 return sm;
351             }
352 
353             Log.d(TAG, "Creating a new state machine for " + device);
354             sm = new BatteryStateMachine(this, device, mStateMachinesThread.getLooper());
355             mStateMachines.put(device, sm);
356             return sm;
357         }
358     }
359 
360     /** Process a change in the bonding state for a device */
handleBondStateChanged(BluetoothDevice device, int fromState, int toState)361     public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) {
362         mHandler.post(() -> bondStateChanged(device, toState));
363     }
364 
365     /** Remove state machine if the bonding for a device is removed */
bondStateChanged(BluetoothDevice device, int bondState)366     private void bondStateChanged(BluetoothDevice device, int bondState) {
367         Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
368         // Remove state machine if the bonding for a device is removed
369         if (bondState != BluetoothDevice.BOND_NONE) {
370             return;
371         }
372 
373         synchronized (mStateMachines) {
374             BatteryStateMachine sm = mStateMachines.get(device);
375             if (sm == null) {
376                 return;
377             }
378             if (sm.getConnectionState() != STATE_DISCONNECTED) {
379                 return;
380             }
381             removeStateMachine(device);
382         }
383     }
384 
removeStateMachine(BluetoothDevice device)385     private void removeStateMachine(BluetoothDevice device) {
386         synchronized (mStateMachines) {
387             BatteryStateMachine sm = mStateMachines.remove(device);
388             if (sm == null) {
389                 Log.w(TAG, "removeStateMachine: " + device + " does not have a state machine");
390                 return;
391             }
392             Log.i(TAG, "removeGatt: removing bluetooth gatt for device: " + device);
393             sm.doQuit();
394             sm.cleanup();
395         }
396     }
397 
398     @Override
dump(StringBuilder sb)399     public void dump(StringBuilder sb) {
400         super.dump(sb);
401         synchronized (mStateMachines) {
402             for (BatteryStateMachine sm : mStateMachines.values()) {
403                 sm.dump(sb);
404             }
405         }
406     }
407 }
408