summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android/app/AndroidManifest.xml9
-rw-r--r--android/app/res/values/config.xml1
-rw-r--r--android/app/src/com/android/bluetooth/bas/BatteryService.java652
-rw-r--r--android/app/src/com/android/bluetooth/bas/BatteryStateMachine.java581
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterService.java38
-rw-r--r--android/app/src/com/android/bluetooth/btservice/Config.java41
-rw-r--r--android/app/src/com/android/bluetooth/btservice/PhonePolicy.java22
-rw-r--r--android/app/src/com/android/bluetooth/btservice/RemoteDevices.java9
-rw-r--r--android/app/src/com/android/bluetooth/btservice/ServiceFactory.java5
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/Metadata.java5
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java23
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java5
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/bas/BatteryServiceTest.java248
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/bas/BatteryStateMachineTest.java243
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java23
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/112.json322
-rw-r--r--framework/java/android/bluetooth/BluetoothProfile.java9
-rw-r--r--framework/java/android/bluetooth/BluetoothUuid.java4
-rw-r--r--system/binder/Android.bp1
-rw-r--r--system/binder/android/bluetooth/IBluetoothBattery.aidl45
-rw-r--r--system/btif/src/btif_dm.cc4
21 files changed, 2264 insertions, 26 deletions
diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml
index e55f2ee4a9..011ed6609e 100644
--- a/android/app/AndroidManifest.xml
+++ b/android/app/AndroidManifest.xml
@@ -497,5 +497,14 @@
<action android:name="android.telecom.InCallService"/>
</intent-filter>
</service>
+ <service
+ android:process="@string/process"
+ android:name=".bas.BatteryService"
+ android:enabled="@bool/profile_supported_battery"
+ android:exported = "true">
+ <intent-filter>
+ <action android:name="android.bluetooth.IBluetoothBattery" />
+ </intent-filter>
+ </service>
</application>
</manifest>
diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml
index ea18d15133..092860759f 100644
--- a/android/app/res/values/config.xml
+++ b/android/app/res/values/config.xml
@@ -40,6 +40,7 @@
<bool name="profile_supported_le_call_control">true</bool>
<bool name="profile_supported_hap_client">true</bool>
<bool name="profile_supported_bass_client">false</bool>
+ <bool name="profile_supported_battery">true</bool>
<!-- If true, we will require location to be enabled on the device to
fire Bluetooth LE scan result callbacks in addition to having one
diff --git a/android/app/src/com/android/bluetooth/bas/BatteryService.java b/android/app/src/com/android/bluetooth/bas/BatteryService.java
new file mode 100644
index 0000000000..9b1df394c1
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/bas/BatteryService.java
@@ -0,0 +1,652 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.bas;
+
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+
+import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
+
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.IBluetoothBattery;
+import android.content.AttributionSource;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.HandlerThread;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A profile service that connects to the Battery service (BAS) of BLE devices
+ */
+public class BatteryService extends ProfileService {
+ private static final boolean DBG = false;
+ private static final String TAG = "BatteryService";
+
+ // Timeout for state machine thread join, to prevent potential ANR.
+ private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1_000;
+
+ private static final int MAX_BATTERY_STATE_MACHINES = 10;
+ private static BatteryService sBatteryService;
+
+ private AdapterService mAdapterService;
+ private HandlerThread mStateMachinesThread;
+ private final Map<BluetoothDevice, BatteryStateMachine> mStateMachines = new HashMap<>();
+
+ private BroadcastReceiver mBondStateChangedReceiver;
+
+ @Override
+ protected IProfileServiceBinder initBinder() {
+ return new BluetoothBatteryBinder(this);
+ }
+
+ @Override
+ protected void create() {
+ if (DBG) {
+ Log.d(TAG, "create()");
+ }
+ }
+
+ @Override
+ protected boolean start() {
+ if (DBG) {
+ Log.d(TAG, "start()");
+ }
+ if (sBatteryService != null) {
+ throw new IllegalStateException("start() called twice");
+ }
+
+ mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
+ "AdapterService cannot be null when BatteryService starts");
+
+ mStateMachines.clear();
+ mStateMachinesThread = new HandlerThread("BatteryService.StateMachines");
+ mStateMachinesThread.start();
+
+ // Setup broadcast receivers
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ mBondStateChangedReceiver = new BondStateChangedReceiver();
+ registerReceiver(mBondStateChangedReceiver, filter);
+
+ setBatteryService(this);
+
+ return true;
+ }
+
+ @Override
+ protected boolean stop() {
+ if (DBG) {
+ Log.d(TAG, "stop()");
+ }
+ if (sBatteryService == null) {
+ Log.w(TAG, "stop() called before start()");
+ return true;
+ }
+
+ setBatteryService(null);
+ // Unregister broadcast receivers
+ unregisterReceiver(mBondStateChangedReceiver);
+ mBondStateChangedReceiver = null;
+
+ // Destroy state machines and stop handler thread
+ synchronized (mStateMachines) {
+ for (BatteryStateMachine sm : mStateMachines.values()) {
+ sm.doQuit();
+ sm.cleanup();
+ }
+ mStateMachines.clear();
+ }
+
+
+ if (mStateMachinesThread != null) {
+ try {
+ mStateMachinesThread.quitSafely();
+ mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
+ mStateMachinesThread = null;
+ } catch (InterruptedException e) {
+ // Do not rethrow as we are shutting down anyway
+ }
+ }
+
+ mAdapterService = null;
+
+ return true;
+ }
+
+ @Override
+ protected void cleanup() {
+ if (DBG) {
+ Log.d(TAG, "cleanup()");
+ }
+ }
+
+ /**
+ * Gets the BatteryService instance
+ */
+ public static synchronized BatteryService getBatteryService() {
+ if (sBatteryService == null) {
+ Log.w(TAG, "getBatteryService(): service is NULL");
+ return null;
+ }
+
+ if (!sBatteryService.isAvailable()) {
+ Log.w(TAG, "getBatteryService(): service is not available");
+ return null;
+ }
+ return sBatteryService;
+ }
+
+ private static synchronized void setBatteryService(BatteryService instance) {
+ if (DBG) {
+ Log.d(TAG, "setBatteryService(): set to: " + instance);
+ }
+ sBatteryService = instance;
+ }
+
+ /**
+ * Connects to the battery service of the given device.
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean connect(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ if (DBG) {
+ Log.d(TAG, "connect(): " + device);
+ }
+ if (device == null) {
+ Log.w(TAG, "Ignore connecting to null device");
+ return false;
+ }
+
+ if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ Log.w(TAG, "Cannot connect to " + device + " : policy forbidden");
+ return false;
+ }
+ ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
+ if (!Utils.arrayContains(featureUuids, BluetoothUuid.BATTERY)) {
+ Log.e(TAG, "Cannot connect to " + device
+ + " : Remote does not have Battery UUID");
+ return false;
+ }
+
+ synchronized (mStateMachines) {
+ BatteryStateMachine sm = getOrCreateStateMachine(device);
+ if (sm == null) {
+ Log.e(TAG, "Cannot connect to " + device + " : no state machine");
+ }
+ sm.sendMessage(BatteryStateMachine.CONNECT);
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnects from the battery service of the given device.
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean disconnect(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ if (DBG) {
+ Log.d(TAG, "disconnect(): " + device);
+ }
+ if (device == null) {
+ Log.w(TAG, "Ignore disconnecting to null device");
+ return false;
+ }
+ synchronized (mStateMachines) {
+ BatteryStateMachine sm = getOrCreateStateMachine(device);
+ if (sm != null) {
+ sm.sendMessage(BatteryStateMachine.DISCONNECT);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets devices that battery service is connected.
+ * @return
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public List<BluetoothDevice> getConnectedDevices() {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ synchronized (mStateMachines) {
+ List<BluetoothDevice> devices = new ArrayList<>();
+ for (BatteryStateMachine sm : mStateMachines.values()) {
+ if (sm.isConnected()) {
+ devices.add(sm.getDevice());
+ }
+ }
+ return devices;
+ }
+ }
+
+ /**
+ * Check whether it can connect to a peer device.
+ * The check considers a number of factors during the evaluation.
+ *
+ * @param device the peer device to connect to
+ * @return true if connection is allowed, otherwise false
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public boolean canConnect(BluetoothDevice device) {
+ // Check connectionPolicy and accept or reject the connection.
+ int connectionPolicy = getConnectionPolicy(device);
+ int bondState = mAdapterService.getBondState(device);
+ // Allow this connection only if the device is bonded. Any attempt to connect while
+ // bonding would potentially lead to an unauthorized connection.
+ if (bondState != BluetoothDevice.BOND_BONDED) {
+ Log.w(TAG, "canConnect: return false, bondState=" + bondState);
+ return false;
+ } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN
+ && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ // Otherwise, reject the connection if connectionPolicy is not valid.
+ Log.w(TAG, "canConnect: return false, connectionPolicy=" + connectionPolicy);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Called when the connection state of a state machine is changed
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void handleConnectionStateChanged(BatteryStateMachine sm,
+ int fromState, int toState) {
+ BluetoothDevice device = sm.getDevice();
+ if ((sm == null) || (fromState == toState)) {
+ Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device
+ + " fromState=" + fromState + " toState=" + toState);
+ return;
+ }
+
+ // Check if the device is disconnected - if unbonded, remove the state machine
+ if (toState == BluetoothProfile.STATE_DISCONNECTED) {
+ int bondState = mAdapterService.getBondState(device);
+ if (bondState == BluetoothDevice.BOND_NONE) {
+ if (DBG) {
+ Log.d(TAG, device + " is unbonded. Remove state machine");
+ }
+ removeStateMachine(device);
+ }
+ }
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ ArrayList<BluetoothDevice> devices = new ArrayList<>();
+ if (states == null) {
+ return devices;
+ }
+ final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+ if (bondedDevices == null) {
+ return devices;
+ }
+ synchronized (mStateMachines) {
+ for (BluetoothDevice device : bondedDevices) {
+ int connectionState = BluetoothProfile.STATE_DISCONNECTED;
+ BatteryStateMachine sm = mStateMachines.get(device);
+ if (sm != null) {
+ connectionState = sm.getConnectionState();
+ }
+ for (int state : states) {
+ if (connectionState == state) {
+ devices.add(device);
+ break;
+ }
+ }
+ }
+ return devices;
+ }
+ }
+
+ /**
+ * Get the list of devices that have state machines.
+ *
+ * @return the list of devices that have state machines
+ */
+ @VisibleForTesting
+ List<BluetoothDevice> getDevices() {
+ List<BluetoothDevice> devices = new ArrayList<>();
+ synchronized (mStateMachines) {
+ for (BatteryStateMachine sm : mStateMachines.values()) {
+ devices.add(sm.getDevice());
+ }
+ return devices;
+ }
+ }
+
+ /**
+ * Gets the connection state of the given device's battery service
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_CONNECT,
+ "Need BLUETOOTH_CONNECT permission");
+ synchronized (mStateMachines) {
+ BatteryStateMachine sm = mStateMachines.get(device);
+ if (sm == null) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ return sm.getConnectionState();
+ }
+ }
+
+ /**
+ * Set connection policy of the profile and connects it if connectionPolicy is
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of:
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
+ * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device the remote device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true on success, otherwise false
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ if (DBG) {
+ Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
+ }
+ mAdapterService.getDatabase()
+ .setProfileConnectionPolicy(device, BluetoothProfile.BATTERY,
+ connectionPolicy);
+ if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ connect(device);
+ } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ disconnect(device);
+ }
+ return true;
+ }
+
+ /**
+ * Gets the connection policy for the battery service of the given device.
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public int getConnectionPolicy(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ return mAdapterService.getDatabase()
+ .getProfileConnectionPolicy(device, BluetoothProfile.BATTERY);
+ }
+
+ void handleBatteryChanged(BluetoothDevice device, int batteryLevel) {
+ mAdapterService.setBatteryLevel(device, batteryLevel);
+ }
+
+ private BatteryStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+ if (device == null) {
+ Log.e(TAG, "getOrCreateGatt failed: device cannot be null");
+ return null;
+ }
+ synchronized (mStateMachines) {
+ BatteryStateMachine sm = mStateMachines.get(device);
+ if (sm != null) {
+ return sm;
+ }
+ // Limit the maximum number of state machines to avoid DoS attack
+ if (mStateMachines.size() >= MAX_BATTERY_STATE_MACHINES) {
+ Log.e(TAG, "Maximum number of Battery state machines reached: "
+ + MAX_BATTERY_STATE_MACHINES);
+ return null;
+ }
+ if (DBG) {
+ Log.d(TAG, "Creating a new state machine for " + device);
+ }
+ sm = BatteryStateMachine.make(device, this, mStateMachinesThread.getLooper());
+ mStateMachines.put(device, sm);
+ return sm;
+ }
+ }
+
+ // Remove state machine if the bonding for a device is removed
+ private class BondStateChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+ int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.ERROR);
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
+ handleBondStateChanged(device, state);
+ }
+ }
+
+ /**
+ * Process a change in the bonding state for a device.
+ *
+ * @param device the device whose bonding state has changed
+ * @param bondState the new bond state for the device. Possible values are:
+ * {@link BluetoothDevice#BOND_NONE},
+ * {@link BluetoothDevice#BOND_BONDING},
+ * {@link BluetoothDevice#BOND_BONDED},
+ * {@link BluetoothDevice#ERROR}.
+ */
+ @VisibleForTesting
+ void handleBondStateChanged(BluetoothDevice device, int bondState) {
+ if (DBG) {
+ Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
+ }
+ // Remove state machine if the bonding for a device is removed
+ if (bondState != BluetoothDevice.BOND_NONE) {
+ return;
+ }
+
+ synchronized (mStateMachines) {
+ BatteryStateMachine sm = mStateMachines.get(device);
+ if (sm == null) {
+ return;
+ }
+ if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
+ return;
+ }
+ removeStateMachine(device);
+ }
+ }
+
+ private void removeStateMachine(BluetoothDevice device) {
+ if (device == null) {
+ Log.e(TAG, "removeStateMachine failed: device cannot be null");
+ return;
+ }
+ synchronized (mStateMachines) {
+ BatteryStateMachine sm = mStateMachines.remove(device);
+ if (sm == null) {
+ Log.w(TAG, "removeStateMachine: device " + device
+ + " does not have a state machine");
+ return;
+ }
+ Log.i(TAG, "removeGatt: removing bluetooth gatt for device: " + device);
+ sm.doQuit();
+ sm.cleanup();
+ }
+ }
+
+ /**
+ * Binder object: must be a static class or memory leak may occur
+ */
+ @VisibleForTesting
+ static class BluetoothBatteryBinder extends IBluetoothBattery.Stub
+ implements IProfileServiceBinder {
+ private final WeakReference<BatteryService> mServiceRef;
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private BatteryService getService(AttributionSource source) {
+ BatteryService service = mServiceRef.get();
+
+ if (!Utils.checkCallerIsSystemOrActiveUser(TAG)
+ || !Utils.checkServiceAvailable(service, TAG)
+ || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
+ return null;
+ }
+ return service;
+ }
+
+ BluetoothBatteryBinder(BatteryService svc) {
+ mServiceRef = new WeakReference<>(svc);
+ }
+
+ @Override
+ public void cleanup() {
+ mServiceRef.clear();
+ }
+
+ @Override
+ public void connect(BluetoothDevice device, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ boolean result = false;
+ if (service != null) {
+ result = service.connect(device);
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void disconnect(BluetoothDevice device, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ boolean result = false;
+ if (service != null) {
+ result = service.disconnect(device);
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void getConnectedDevices(AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ List<BluetoothDevice> result = new ArrayList<>();
+ if (service != null) {
+ enforceBluetoothPrivilegedPermission(service);
+ result = service.getConnectedDevices();
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void getDevicesMatchingConnectionStates(int[] states,
+ AttributionSource source, SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ List<BluetoothDevice> result = new ArrayList<>();
+ if (service != null) {
+ result = service.getDevicesMatchingConnectionStates(states);
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void getConnectionState(BluetoothDevice device, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ int result = BluetoothProfile.STATE_DISCONNECTED;
+ if (service != null) {
+ result = service.getConnectionState(device);
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
+ AttributionSource source, SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ boolean result = false;
+ if (service != null) {
+ result = service.setConnectionPolicy(device, connectionPolicy);
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+
+ @Override
+ public void getConnectionPolicy(BluetoothDevice device, AttributionSource source,
+ SynchronousResultReceiver receiver) {
+ try {
+ BatteryService service = getService(source);
+ int result = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ if (service != null) {
+ result = service.getConnectionPolicy(device);
+ }
+ receiver.send(result);
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+ }
+
+ @Override
+ public void dump(StringBuilder sb) {
+ super.dump(sb);
+ for (BatteryStateMachine sm : mStateMachines.values()) {
+ sm.dump(sb);
+ }
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/bas/BatteryStateMachine.java b/android/app/src/com/android/bluetooth/bas/BatteryStateMachine.java
new file mode 100644
index 0000000000..afc6c5a195
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/bas/BatteryStateMachine.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.bas;
+
+import static android.bluetooth.BluetoothDevice.PHY_LE_1M_MASK;
+import static android.bluetooth.BluetoothDevice.PHY_LE_2M_MASK;
+import static android.bluetooth.BluetoothDevice.TRANSPORT_AUTO;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.util.Scanner;
+import java.util.UUID;
+
+/**
+ * It manages Battery service of a BLE device
+ */
+public class BatteryStateMachine extends StateMachine {
+ private static final boolean DBG = false;
+ private static final String TAG = "BatteryStateMachine";
+
+ static final UUID GATT_BATTERY_SERVICE_UUID =
+ UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
+
+ static final UUID GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID =
+ UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
+
+ static final int CONNECT = 1;
+ static final int DISCONNECT = 2;
+ static final int CONNECTION_STATE_CHANGED = 3;
+ private static final int CONNECT_TIMEOUT = 201;
+
+ // NOTE: the value is not "final" - it is modified in the unit tests
+ @VisibleForTesting
+ static int sConnectTimeoutMs = 30000; // 30s
+
+ private Disconnected mDisconnected;
+ private Connecting mConnecting;
+ private Connected mConnected;
+ private Disconnecting mDisconnecting;
+ private int mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+
+ WeakReference<BatteryService> mServiceRef;
+
+ BluetoothGatt mBluetoothGatt;
+ BluetoothGattCallback mGattCallback;
+ final BluetoothDevice mDevice;
+
+ BatteryStateMachine(BluetoothDevice device, BatteryService service, Looper looper) {
+ super(TAG, looper);
+ mDevice = device;
+ mServiceRef = new WeakReference<>(service);
+
+ mDisconnected = new Disconnected();
+ mConnecting = new Connecting();
+ mConnected = new Connected();
+ mDisconnecting = new Disconnecting();
+
+ addState(mDisconnected);
+ addState(mConnecting);
+ addState(mDisconnecting);
+ addState(mConnected);
+
+ setInitialState(mDisconnected);
+ }
+
+ static BatteryStateMachine make(BluetoothDevice device, BatteryService service, Looper looper) {
+ Log.i(TAG, "make for device " + device);
+ BatteryStateMachine sm = new BatteryStateMachine(device, service, looper);
+ sm.start();
+ return sm;
+ }
+
+ /**
+ * Quits the state machine
+ */
+ public void doQuit() {
+ log("doQuit for device " + mDevice);
+ quitNow();
+ }
+
+ /**
+ * Cleans up the resources the state machine held.
+ */
+ public void cleanup() {
+ log("cleanup for device " + mDevice);
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.close();
+ mBluetoothGatt = null;
+ mGattCallback = null;
+ }
+ }
+
+ BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ synchronized boolean isConnected() {
+ return getCurrentState() == mConnected;
+ }
+
+ private static String messageWhatToString(int what) {
+ switch (what) {
+ case CONNECT:
+ return "CONNECT";
+ case DISCONNECT:
+ return "DISCONNECT";
+ case CONNECTION_STATE_CHANGED:
+ return "CONNECTION_STATE_CHANGED";
+ case CONNECT_TIMEOUT:
+ return "CONNECT_TIMEOUT";
+ default:
+ break;
+ }
+ return Integer.toString(what);
+ }
+
+ private static String profileStateToString(int state) {
+ switch (state) {
+ case BluetoothProfile.STATE_DISCONNECTED:
+ return "DISCONNECTED";
+ case BluetoothProfile.STATE_CONNECTING:
+ return "CONNECTING";
+ case BluetoothProfile.STATE_CONNECTED:
+ return "CONNECTED";
+ case BluetoothProfile.STATE_DISCONNECTING:
+ return "DISCONNECTING";
+ default:
+ break;
+ }
+ return Integer.toString(state);
+ }
+
+ /**
+ * Dumps battery state machine state.
+ */
+ public void dump(StringBuilder sb) {
+ ProfileService.println(sb, "mDevice: " + mDevice);
+ ProfileService.println(sb, " StateMachine: " + this);
+ ProfileService.println(sb, " BluetoothGatt: " + mBluetoothGatt);
+ // Dump the state machine logs
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ super.dump(new FileDescriptor(), printWriter, new String[]{});
+ printWriter.flush();
+ stringWriter.flush();
+ ProfileService.println(sb, " StateMachineLog:");
+ Scanner scanner = new Scanner(stringWriter.toString());
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine();
+ ProfileService.println(sb, " " + line);
+ }
+ scanner.close();
+ }
+
+ @BluetoothProfile.BtProfileState
+ int getConnectionState() {
+ String currentState = getCurrentState().getName();
+ switch (currentState) {
+ case "Disconnected":
+ return BluetoothProfile.STATE_DISCONNECTED;
+ case "Connecting":
+ return BluetoothProfile.STATE_CONNECTING;
+ case "Connected":
+ return BluetoothProfile.STATE_CONNECTED;
+ case "Disconnecting":
+ return BluetoothProfile.STATE_DISCONNECTING;
+ default:
+ Log.e(TAG, "Bad currentState: " + currentState);
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ }
+
+ void dispatchConnectionStateChanged(int fromState, int toState) {
+ log("Connection state " + mDevice + ": " + profileStateToString(fromState)
+ + "->" + profileStateToString(toState));
+
+ BatteryService service = mServiceRef.get();
+ if (service != null) {
+ service.handleConnectionStateChanged(this, fromState, toState);
+ }
+ }
+
+ /**
+ * Connects to the GATT server of the device.
+ *
+ * @return {@code true} if it successfully connects to the GATT server.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public boolean connectGatt() {
+ BatteryService service = mServiceRef.get();
+ if (service == null) {
+ return false;
+ }
+
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.connect();
+ } else {
+ mGattCallback = new GattCallback();
+ mBluetoothGatt = mDevice.connectGatt(service, /*autoConnect=*/true,
+ mGattCallback, TRANSPORT_AUTO, /*opportunistic=*/true,
+ PHY_LE_1M_MASK | PHY_LE_2M_MASK, getHandler());
+ }
+ return mBluetoothGatt != null;
+ }
+
+ @Override
+ protected void log(String msg) {
+ if (DBG) {
+ super.log(msg);
+ }
+ }
+
+ static void log(String tag, String msg) {
+ if (DBG) {
+ Log.d(tag, msg);
+ }
+ }
+
+ @VisibleForTesting
+ class Disconnected extends State {
+ private static final String TAG = "BASM_Disconnected";
+
+ @Override
+ public void enter() {
+ log(TAG, "Enter (" + mDevice + "): " + messageWhatToString(
+ getCurrentMessage().what));
+
+ if (mLastConnectionState != BluetoothProfile.STATE_DISCONNECTED) {
+ // Don't broadcast during startup
+ dispatchConnectionStateChanged(mLastConnectionState,
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+ }
+
+ @Override
+ public void exit() {
+ log(TAG, "Exit (" + mDevice + "): " + messageWhatToString(
+ getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log(TAG, "Process message(" + mDevice + "): " + messageWhatToString(
+ message.what));
+
+ BatteryService service = mServiceRef.get();
+ switch (message.what) {
+ case CONNECT:
+ log(TAG, "Connecting to " + mDevice);
+ if (service != null && service.canConnect(mDevice)) {
+ if (connectGatt()) {
+ transitionTo(mConnecting);
+ } else {
+ Log.w(TAG, "Battery connecting request rejected due to "
+ + "GATT connection rejection: " + mDevice);
+ }
+ } else {
+ // Reject the request and stay in Disconnected state
+ Log.w(TAG, "Battery connecting request rejected: "
+ + mDevice);
+ }
+ break;
+ case DISCONNECT:
+ Log.w(TAG, "DISCONNECT ignored: " + mDevice);
+ break;
+ case CONNECTION_STATE_CHANGED:
+ processConnectionEvent(message.arg1);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Disconnected state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case BluetoothGatt.STATE_DISCONNECTED:
+ Log.w(TAG, "Ignore Battery DISCONNECTED event: " + mDevice);
+ break;
+ default:
+ Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice);
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class Connecting extends State {
+ private static final String TAG = "BASM_Connecting";
+ @Override
+ public void enter() {
+ log(TAG, "Enter (" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+ dispatchConnectionStateChanged(mLastConnectionState, BluetoothProfile.STATE_CONNECTING);
+ }
+
+ @Override
+ public void exit() {
+ log(TAG, "Exit (" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_CONNECTING;
+ removeMessages(CONNECT_TIMEOUT);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log(TAG, "process message(" + mDevice + "): "
+ + messageWhatToString(message.what));
+
+ switch (message.what) {
+ case CONNECT:
+ Log.w(TAG, "CONNECT ignored: " + mDevice);
+ break;
+ case CONNECT_TIMEOUT:
+ Log.w(TAG, "Connection timeout: " + mDevice);
+ // fall through
+ case DISCONNECT:
+ log(TAG, "Connection canceled to " + mDevice);
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.disconnect();
+ transitionTo(mDisconnecting);
+ } else {
+ transitionTo(mDisconnected);
+ }
+ break;
+ case CONNECTION_STATE_CHANGED:
+ processConnectionEvent(message.arg1);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Connecting state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case BluetoothGatt.STATE_DISCONNECTED:
+ Log.w(TAG, "Device disconnected: " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case BluetoothGatt.STATE_CONNECTED:
+ transitionTo(mConnected);
+ break;
+ default:
+ Log.e(TAG, "Incorrect state: " + state);
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class Disconnecting extends State {
+ private static final String TAG = "BASM_Disconnecting";
+ @Override
+ public void enter() {
+ log(TAG, "Enter (" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+ dispatchConnectionStateChanged(mLastConnectionState,
+ BluetoothProfile.STATE_DISCONNECTING);
+ }
+
+ @Override
+ public void exit() {
+ log(TAG, "Exit (" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING;
+ removeMessages(CONNECT_TIMEOUT);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log(TAG, "Process message(" + mDevice + "): "
+ + messageWhatToString(message.what));
+
+ switch (message.what) {
+ //TODO: Check if connect while disconnecting is okay.
+ // It is related to CONNECT_TIMEOUT as well.
+ case CONNECT:
+ Log.w(TAG, "CONNECT ignored: " + mDevice);
+ break;
+ case DISCONNECT:
+ Log.w(TAG, "DISCONNECT ignored: " + mDevice);
+ break;
+ case CONNECT_TIMEOUT:
+ Log.w(TAG, "Connection timeout: " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case CONNECTION_STATE_CHANGED:
+ processConnectionEvent(message.arg1);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Disconnecting state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case BluetoothGatt.STATE_DISCONNECTED:
+ Log.i(TAG, "Disconnected: " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case BluetoothGatt.STATE_CONNECTED: {
+ // Reject the connection and stay in Disconnecting state
+ Log.w(TAG, "Incoming Battery connected request rejected: "
+ + mDevice);
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.disconnect();
+ } else {
+ transitionTo(mDisconnected);
+ }
+ break;
+ }
+ default:
+ Log.e(TAG, "Incorrect state: " + state);
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class Connected extends State {
+ private static final String TAG = "BASM_Connected";
+ @Override
+ public void enter() {
+ log(TAG, "Enter (" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ dispatchConnectionStateChanged(mLastConnectionState, BluetoothProfile.STATE_CONNECTED);
+
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.discoverServices();
+ }
+ }
+
+ @Override
+ public void exit() {
+ log(TAG, "Exit (" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log(TAG, "Process message(" + mDevice + "): "
+ + messageWhatToString(message.what));
+
+ switch (message.what) {
+ case CONNECT:
+ Log.w(TAG, "CONNECT ignored: " + mDevice);
+ break;
+ case DISCONNECT:
+ log(TAG, "Disconnecting from " + mDevice);
+ if (mBluetoothGatt != null) {
+ mBluetoothGatt.disconnect();
+ transitionTo(mDisconnecting);
+ } else {
+ transitionTo(mDisconnected);
+ }
+ break;
+ case CONNECTION_STATE_CHANGED:
+ processConnectionEvent(message.arg1);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Connected state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case BluetoothGatt.STATE_DISCONNECTED:
+ Log.i(TAG, "Disconnected from " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case BluetoothGatt.STATE_CONNECTED:
+ Log.w(TAG, "Ignore CONNECTED event: " + mDevice);
+ break;
+ default:
+ Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state);
+ break;
+ }
+ }
+ }
+
+ final class GattCallback extends BluetoothGattCallback {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ sendMessage(CONNECTION_STATE_CHANGED, newState);
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "No gatt service");
+ return;
+ }
+
+ final BluetoothGattService batteryService = gatt.getService(GATT_BATTERY_SERVICE_UUID);
+ if (batteryService == null) {
+ Log.e(TAG, "No battery service");
+ return;
+ }
+
+ final BluetoothGattCharacteristic batteryLevel =
+ batteryService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID);
+ if (batteryLevel == null) {
+ Log.e(TAG, "No battery level characteristic");
+ return;
+ }
+
+ gatt.setCharacteristicNotification(batteryLevel, /*enable=*/true);
+ gatt.readCharacteristic(batteryLevel);
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, byte[] value) {
+ if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
+ updateBatteryLevel(value);
+ }
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, byte[] value, int status) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic);
+ return;
+ }
+
+ if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
+ updateBatteryLevel(value);
+ }
+ }
+
+ private void updateBatteryLevel(byte[] value) {
+ int batteryLevel = value[0] & 0xFF;
+
+ BatteryService service = mServiceRef.get();
+ service.handleBatteryChanged(mDevice, batteryLevel);
+ }
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index e68abd7052..503ae7c1e5 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -45,8 +45,6 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter.ActiveDeviceProfile;
import android.bluetooth.BluetoothAdapter.ActiveDeviceUse;
import android.bluetooth.BluetoothClass;
-import android.bluetooth.BluetoothCodecConfig;
-import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProtoEnums;
@@ -104,6 +102,7 @@ import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.a2dpsink.A2dpSinkService;
+import com.android.bluetooth.bas.BatteryService;
import com.android.bluetooth.bass_client.BassClientService;
import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
import com.android.bluetooth.btservice.activityattribution.ActivityAttributionService;
@@ -328,6 +327,7 @@ public class AdapterService extends Service {
private CsipSetCoordinatorService mCsipSetCoordinatorService;
private LeAudioService mLeAudioService;
private BassClientService mBassClientService;
+ private BatteryService mBatteryService;
private BinderCallsStats.SettingsObserver mBinderCallsSettingsObserver;
@@ -372,7 +372,7 @@ public class AdapterService extends Service {
/**
* Confirm whether the ProfileService is started expectedly.
*
- * @param string the service simple name.
+ * @param serviceSampleName the service simple name.
* @return true if the service is started expectedly, false otherwise.
*/
public boolean isStartedProfile(String serviceSampleName) {
@@ -1102,6 +1102,9 @@ public class AdapterService extends Service {
if (profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
return Utils.arrayContains(remoteDeviceUuids, BluetoothUuid.BASS);
}
+ if (profile == BluetoothProfile.BATTERY) {
+ return Utils.arrayContains(remoteDeviceUuids, BluetoothUuid.BATTERY);
+ }
Log.e(TAG, "isSupported: Unexpected profile passed in to function: " + profile);
return false;
@@ -1115,7 +1118,6 @@ public class AdapterService extends Service {
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
boolean isAnyProfileEnabled(BluetoothDevice device) {
-
if (mA2dpService != null && mA2dpService.getConnectionPolicy(device)
> BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
return true;
@@ -1173,12 +1175,16 @@ public class AdapterService extends Service {
> BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
return true;
}
+ if (mBatteryService != null && mBatteryService.getConnectionPolicy(device)
+ > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ return true;
+ }
return false;
}
/**
* Connects only available profiles
- * (those with {@link BluetoothProfile.CONNECTION_POLICY_ALLOWED})
+ * (those with {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED})
*
* @param device is the device with which we are connecting the profiles
* @return {@link BluetoothStatusCodes#SUCCESS}
@@ -1274,17 +1280,25 @@ public class AdapterService extends Service {
if (mLeAudioService != null && isSupported(localDeviceUuids, remoteDeviceUuids,
BluetoothProfile.LE_AUDIO, device)
&& mLeAudioService.getConnectionPolicy(device)
- > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
Log.i(TAG, "connectEnabledProfiles: Connecting LeAudio profile (BAP)");
mLeAudioService.connect(device);
}
if (mBassClientService != null && isSupported(localDeviceUuids, remoteDeviceUuids,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, device)
&& mBassClientService.getConnectionPolicy(device)
- > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
Log.i(TAG, "connectEnabledProfiles: Connecting LE Broadcast Assistant Profile");
mBassClientService.connect(device);
}
+ if (mBatteryService != null
+ && isSupported(
+ localDeviceUuids, remoteDeviceUuids, BluetoothProfile.BATTERY, device)
+ && mBatteryService.getConnectionPolicy(device)
+ > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ Log.i(TAG, "connectEnabledProfiles: Connecting Battery Service");
+ mBatteryService.connect(device);
+ }
return BluetoothStatusCodes.SUCCESS;
}
@@ -1326,6 +1340,7 @@ public class AdapterService extends Service {
mCsipSetCoordinatorService = CsipSetCoordinatorService.getCsipSetCoordinatorService();
mLeAudioService = LeAudioService.getLeAudioService();
mBassClientService = BassClientService.getBassClientService();
+ mBatteryService = BatteryService.getBatteryService();
}
@BluetoothAdapter.RfcommListenerResult
@@ -3903,7 +3918,7 @@ public class AdapterService extends Service {
* Fetches the local OOB data to give out to remote.
*
* @param transport - specify data transport.
- * @param callback - callback used to receive the requested {@link Oobdata}; null will be
+ * @param callback - callback used to receive the requested {@link OobData}; null will be
* ignored silently.
*
* @hide
@@ -5270,6 +5285,13 @@ public class AdapterService extends Service {
return allowLowLatencyAudioNative(allowed, Utils.getByteAddress(device));
}
+ /**
+ * Sets the battery level of the remote device
+ */
+ public void setBatteryLevel(BluetoothDevice device, int batteryLevel) {
+ mRemoteDevices.updateBatteryLevel(device, batteryLevel);
+ }
+
static native void classInitNative();
native boolean initNative(boolean startRestricted, boolean isCommonCriteriaMode,
diff --git a/android/app/src/com/android/bluetooth/btservice/Config.java b/android/app/src/com/android/bluetooth/btservice/Config.java
index 9ddd9ab9e0..d7bbafbc40 100644
--- a/android/app/src/com/android/bluetooth/btservice/Config.java
+++ b/android/app/src/com/android/bluetooth/btservice/Config.java
@@ -33,6 +33,7 @@ import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.a2dpsink.A2dpSinkService;
import com.android.bluetooth.avrcp.AvrcpTargetService;
import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
+import com.android.bluetooth.bas.BatteryService;
import com.android.bluetooth.bass_client.BassClientService;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.gatt.GattService;
@@ -63,6 +64,9 @@ import java.util.List;
public class Config {
private static final String TAG = "AdapterServiceConfig";
+ private static final String FEATURE_HEARING_AID = "settings_bluetooth_hearing_aid";
+ private static final String FEATURE_BATTERY = "settings_bluetooth_battery";
+
private static class ProfileConfig {
Class mClass;
int mSupported;
@@ -140,6 +144,8 @@ public class Config {
new ProfileConfig(BassClientService.class,
R.bool.profile_supported_bass_client,
(1 << BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)),
+ new ProfileConfig(BatteryService.class, R.bool.profile_supported_battery,
+ (1 << BluetoothProfile.BATTERY)),
};
private static Class[] sSupportedProfiles = new Class[0];
@@ -163,22 +169,28 @@ public class Config {
supported =
BluetoothProperties.isProfileAshaCentralEnabled().orElse(false);
} else {
- supported = resources.getBoolean(config.mSupported);
+ supported = config.mSupported > 0 ? resources.getBoolean(config.mSupported) : false;
}
- if (!supported && (config.mClass == HearingAidService.class) && isHearingAidSettingsEnabled(ctx)) {
- Log.v(TAG, "Feature Flag enables support for HearingAidService");
+ if (!supported && (config.mClass == HearingAidService.class)
+ && isFeatureEnabled(ctx, FEATURE_HEARING_AID, /*defaultValue=*/false)) {
+ Log.i(TAG, "Feature Flag enables support for HearingAidService");
supported = true;
}
+ if (config.mClass == BatteryService.class) {
+ // It could be overridden by flag.
+ supported = isFeatureEnabled(ctx, FEATURE_BATTERY, /*defaultValue=*/ supported);
+ }
+
if (enabledProfiles != null && enabledProfiles.contains(config.mClass.getName())) {
supported = true;
- Log.v(TAG, config.mClass.getSimpleName() + " Feature Flag set to " + supported
+ Log.i(TAG, config.mClass.getSimpleName() + " Feature Flag set to " + supported
+ " by components configuration");
}
if (supported && !isProfileDisabled(ctx, config.mMask)) {
- Log.v(TAG, "Adding " + config.mClass.getSimpleName());
+ Log.i(TAG, "Adding " + config.mClass.getSimpleName());
profiles.add(config.mClass);
}
}
@@ -247,26 +259,31 @@ public class Config {
final String flagOverridePrefix = "sys.fflag.override.";
final String hearingAidSettings = "settings_bluetooth_hearing_aid";
+ return isFeatureEnabled(context, hearingAidSettings, /*defaultValue=*/false);
+ }
+
+ private static boolean isFeatureEnabled(Context context, String feature, boolean defaultValue) {
+ final String flagOverridePrefix = "sys.fflag.override.";
// Override precedence:
// Settings.Global -> sys.fflag.override.* -> static list
- // Step 1: check if hearing aid flag is set in Settings.Global.
+ // Step 1: check if feature flag is set in Settings.Global.
String value;
if (context != null) {
- value = Settings.Global.getString(context.getContentResolver(), hearingAidSettings);
+ value = Settings.Global.getString(context.getContentResolver(), feature);
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
}
- // Step 2: check if hearing aid flag has any override.
- value = SystemProperties.get(flagOverridePrefix + hearingAidSettings);
+ // Step 2: check if feature flag has any override.
+ // Flag name: sys.fflag.override.<feature>
+ value = SystemProperties.get(flagOverridePrefix + feature);
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
-
- // Step 3: return default value.
- return false;
+ // Step 3: return default value
+ return defaultValue;
}
private static List<String> getSystemConfigEnabledProfilesForPackage(Context ctx) {
diff --git a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java
index 451bd4c887..9b83ccf01b 100644
--- a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java
+++ b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java
@@ -41,6 +41,7 @@ import android.util.Log;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
+import com.android.bluetooth.bas.BatteryService;
import com.android.bluetooth.bass_client.BassClientService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
@@ -300,6 +301,7 @@ class PhonePolicy {
mFactory.getVolumeControlService();
HapClientService hapClientService = mFactory.getHapClientService();
BassClientService bcService = mFactory.getBassClientService();
+ BatteryService batteryService = mFactory.getBatteryService();
// Set profile priorities only for the profiles discovered on the remote device.
// This avoids needless auto-connect attempts to profiles non-existent on the remote device
@@ -385,6 +387,13 @@ class PhonePolicy {
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT,
BluetoothProfile.CONNECTION_POLICY_ALLOWED);
}
+ if ((batteryService != null) && Utils.arrayContains(uuids,
+ BluetoothUuid.BATTERY) && (batteryService.getConnectionPolicy(device)
+ == BluetoothProfile.CONNECTION_POLICY_UNKNOWN)) {
+ debugLog("setting battery profile priority for device " + device);
+ mAdapterService.getDatabase().setProfileConnectionPolicy(device,
+ BluetoothProfile.BATTERY, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ }
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
@@ -594,6 +603,7 @@ class PhonePolicy {
mFactory.getCsipSetCoordinatorService();
VolumeControlService volumeControlService =
mFactory.getVolumeControlService();
+ BatteryService batteryService = mFactory.getBatteryService();
if (hsService != null) {
if (!mHeadsetRetrySet.contains(device) && (hsService.getConnectionPolicy(device)
@@ -653,10 +663,20 @@ class PhonePolicy {
== BluetoothProfile.CONNECTION_POLICY_ALLOWED)
&& (volumeControlService.getConnectionState(device)
== BluetoothProfile.STATE_DISCONNECTED)) {
- debugLog("Retrying connection to CSIP with device " + device);
+ debugLog("Retrying connection to VCP with device " + device);
volumeControlService.connect(device);
}
}
+ if (batteryService != null) {
+ List<BluetoothDevice> connectedDevices = batteryService.getConnectedDevices();
+ if (!connectedDevices.contains(device) && (batteryService.getConnectionPolicy(device)
+ == BluetoothProfile.CONNECTION_POLICY_ALLOWED)
+ && (batteryService.getConnectionState(device)
+ == BluetoothProfile.STATE_DISCONNECTED)) {
+ debugLog("Retrying connection to BAS with device " + device);
+ batteryService.connect(device);
+ }
+ }
}
private static void debugLog(String msg) {
diff --git a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
index c9d8426dcd..122e59ae43 100644
--- a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -41,6 +41,7 @@ import android.util.Log;
import com.android.bluetooth.BluetoothStatsLog;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.bas.BatteryService;
import com.android.bluetooth.hfp.HeadsetHalConstants;
import com.android.internal.annotations.VisibleForTesting;
@@ -765,6 +766,10 @@ final class RemoteDevices {
|| state == BluetoothAdapter.STATE_BLE_TURNING_ON) {
intent = new Intent(BluetoothAdapter.ACTION_BLE_ACL_CONNECTED);
}
+ BatteryService batteryService = BatteryService.getBatteryService();
+ if (batteryService != null) {
+ batteryService.connect(device);
+ }
debugLog(
"aclStateChangeCallback: Adapter State: " + BluetoothAdapter.nameForState(state)
+ " Connected: " + device);
@@ -786,6 +791,10 @@ final class RemoteDevices {
}
// Reset battery level on complete disconnection
if (sAdapterService.getConnectionState(device) == 0) {
+ BatteryService batteryService = BatteryService.getBatteryService();
+ if (batteryService != null) {
+ batteryService.disconnect(device);
+ }
resetBatteryLevel(device);
}
if (!sAdapterService.isAnyProfileEnabled(device)) {
diff --git a/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java b/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java
index 98d37e96a3..e8a624eb95 100644
--- a/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java
+++ b/android/app/src/com/android/bluetooth/btservice/ServiceFactory.java
@@ -18,6 +18,7 @@ package com.android.bluetooth.btservice;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.avrcp.AvrcpTargetService;
+import com.android.bluetooth.bas.BatteryService;
import com.android.bluetooth.bass_client.BassClientService;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.hap.HapClientService;
@@ -83,4 +84,8 @@ public class ServiceFactory {
public BassClientService getBassClientService() {
return BassClientService.getBassClientService();
}
+
+ public BatteryService getBatteryService() {
+ return BatteryService.getBatteryService();
+ }
}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
index 6d917393a3..36d6b43cae 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -128,6 +128,9 @@ class Metadata {
case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
profileConnectionPolicies.bass_client_connection_policy = connectionPolicy;
break;
+ case BluetoothProfile.BATTERY:
+ profileConnectionPolicies.battery_connection_policy = connectionPolicy;
+ break;
default:
throw new IllegalArgumentException("invalid profile " + profile);
}
@@ -171,6 +174,8 @@ class Metadata {
return profileConnectionPolicies.le_call_control_connection_policy;
case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
return profileConnectionPolicies.bass_client_connection_policy;
+ case BluetoothProfile.BATTERY:
+ return profileConnectionPolicies.battery_connection_policy;
}
return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
index c008ef95da..91338179f2 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
@@ -33,7 +33,7 @@ import java.util.List;
/**
* MetadataDatabase is a Room database stores Bluetooth persistence data
*/
-@Database(entities = {Metadata.class}, version = 111)
+@Database(entities = {Metadata.class}, version = 112)
public abstract class MetadataDatabase extends RoomDatabase {
/**
* The metadata database file name
@@ -64,6 +64,7 @@ public abstract class MetadataDatabase extends RoomDatabase {
.addMigrations(MIGRATION_108_109)
.addMigrations(MIGRATION_109_110)
.addMigrations(MIGRATION_110_111)
+ .addMigrations(MIGRATION_111_112)
.allowMainThreadQueries()
.build();
}
@@ -435,7 +436,7 @@ public abstract class MetadataDatabase extends RoomDatabase {
try {
database.execSQL(
"ALTER TABLE metadata ADD COLUMN `bass_client_connection_policy` "
- + "INTEGER DEFAULT 100");
+ + "INTEGER DEFAULT 100");
} catch (SQLException ex) {
// Check if user has new schema, but is just missing the version update
Cursor cursor = database.query("SELECT * FROM metadata");
@@ -446,4 +447,22 @@ public abstract class MetadataDatabase extends RoomDatabase {
}
}
};
+
+ @VisibleForTesting
+ static final Migration MIGRATION_111_112 = new Migration(111, 112) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ try {
+ database.execSQL(
+ "ALTER TABLE metadata ADD COLUMN `battery_connection_policy` "
+ + "INTEGER DEFAULT 100");
+ } catch (SQLException ex) {
+ // Check if user has new schema, but is just missing the version update
+ Cursor cursor = database.query("SELECT * FROM metadata");
+ if (cursor == null || cursor.getColumnIndex("battery_connection_policy") == -1) {
+ throw ex;
+ }
+ }
+ }
+ };
}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
index 4f81f9eeb5..6825adc97a 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
@@ -41,6 +41,7 @@ class ProfilePrioritiesEntity {
public int csip_set_coordinator_connection_policy;
public int le_call_control_connection_policy;
public int bass_client_connection_policy;
+ public int battery_connection_policy;
ProfilePrioritiesEntity() {
a2dp_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
@@ -61,6 +62,7 @@ class ProfilePrioritiesEntity {
csip_set_coordinator_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
le_call_control_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
bass_client_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ battery_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
}
public String toString() {
@@ -82,7 +84,8 @@ class ProfilePrioritiesEntity {
.append("|LE_AUDIO=").append(le_audio_connection_policy)
.append("|VOLUME_CONTROL=").append(volume_control_connection_policy)
.append("|LE_CALL_CONTROL=").append(le_call_control_connection_policy)
- .append("|LE_BROADCAST_ASSISTANT=").append(bass_client_connection_policy);
+ .append("|LE_BROADCAST_ASSISTANT=").append(bass_client_connection_policy)
+ .append("|BATTERY=").append(battery_connection_policy);
return builder.toString();
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/bas/BatteryServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/bas/BatteryServiceTest.java
new file mode 100644
index 0000000000..c8b4e89e2b
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/bas/BatteryServiceTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.bas;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.spy;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.content.Context;
+import android.os.Looper;
+import android.os.ParcelUuid;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ServiceTestRule;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.TimeoutException;
+
+@LargeTest
+@RunWith(JUnit4.class)
+public class BatteryServiceTest {
+ private BluetoothAdapter mAdapter;
+ private Context mTargetContext;
+ private BatteryService mService;
+ private BluetoothDevice mDevice;
+ private static final int CONNECTION_TIMEOUT_MS = 1000;
+
+ @Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
+
+ @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ Assume.assumeTrue("Ignore test when Battery service is not enabled",
+ mTargetContext.getResources().getBoolean(R.bool.profile_supported_battery));
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ TestUtils.setAdapterService(mAdapterService);
+ doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
+ doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ startService();
+
+ // Override the timeout value to speed up the test
+ BatteryStateMachine.sConnectTimeoutMs = CONNECTION_TIMEOUT_MS; // 1s
+
+ // Get a device for testing
+ mDevice = TestUtils.getTestDevice(mAdapter, 0);
+ doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
+ .getBondState(any(BluetoothDevice.class));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mTargetContext.getResources().getBoolean(
+ R.bool.profile_supported_battery)) {
+ return;
+ }
+ stopService();
+ TestUtils.clearAdapterService(mAdapterService);
+ }
+
+ private void startService() throws TimeoutException {
+ TestUtils.startService(mServiceRule, BatteryService.class);
+ mService = BatteryService.getBatteryService();
+ Assert.assertNotNull(mService);
+ }
+
+ private void stopService() throws TimeoutException {
+ TestUtils.stopService(mServiceRule, BatteryService.class);
+ mService = BatteryService.getBatteryService();
+ Assert.assertNull(mService);
+ }
+
+ /**
+ * Test get Battery Service
+ */
+ @Test
+ public void testGetBatteryService() {
+ Assert.assertEquals(mService, BatteryService.getBatteryService());
+ }
+
+ /**
+ * Test get/set policy for BluetoothDevice
+ */
+ @Test
+ public void testGetSetPolicy() {
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ Assert.assertEquals("Initial device policy",
+ BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
+ mService.getConnectionPolicy(mDevice));
+
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ Assert.assertEquals("Setting device policy to POLICY_FORBIDDEN",
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
+ mService.getConnectionPolicy(mDevice));
+
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ Assert.assertEquals("Setting device policy to POLICY_ALLOWED",
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED,
+ mService.getConnectionPolicy(mDevice));
+ }
+
+ /**
+ * Test okToConnect method using various test cases
+ */
+ @Test
+ public void testCanConnect() {
+ int badPolicyValue = 1024;
+ int badBondState = 42;
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, badPolicyValue, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, badPolicyValue, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, true);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_ALLOWED, true);
+ testCanConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, badPolicyValue, false);
+ testCanConnectCase(mDevice,
+ badBondState, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
+ testCanConnectCase(mDevice,
+ badBondState, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testCanConnectCase(mDevice,
+ badBondState, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
+ testCanConnectCase(mDevice,
+ badBondState, badPolicyValue, false);
+ }
+
+ /**
+ * Test that an outgoing connection to device
+ */
+ @Test
+ public void testConnect() {
+ // Update the device policy so okToConnect() returns true
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ // Return Battery UUID
+ doReturn(new ParcelUuid[]{BluetoothUuid.BATTERY}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ // Send a connect request
+ Assert.assertTrue("Connect expected to succeed", mService.connect(mDevice));
+ }
+
+ /**
+ * Test that an outgoing connection to device with POLICY_FORBIDDEN is rejected
+ */
+ @Test
+ public void testForbiddenPolicy_FailsToConnect() {
+ // Set the device policy to POLICY_FORBIDDEN so connect() should fail
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+
+ // Send a connect request
+ Assert.assertFalse("Connect expected to fail", mService.connect(mDevice));
+ }
+
+ /**
+ * Helper function to test okToConnect() method
+ *
+ * @param device test device
+ * @param bondState bond state value, could be invalid
+ * @param policy value, could be invalid
+ * @param expected expected result from okToConnect()
+ */
+ private void testCanConnectCase(BluetoothDevice device, int bondState, int policy,
+ boolean expected) {
+ doReturn(bondState).when(mAdapterService).getBondState(device);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.BATTERY))
+ .thenReturn(policy);
+ Assert.assertEquals(expected, mService.canConnect(device));
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/bas/BatteryStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/bas/BatteryStateMachineTest.java
new file mode 100644
index 0000000000..2f9c48e052
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/bas/BatteryStateMachineTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.bas;
+
+import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.reset;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+
+import org.hamcrest.core.IsInstanceOf;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@LargeTest
+@RunWith(JUnit4.class)
+public class BatteryStateMachineTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private BluetoothAdapter mAdapter;
+ private Context mTargetContext;
+ private HandlerThread mHandlerThread;
+ private StubBatteryStateMachine mBatteryStateMachine;
+ private static final int CONNECTION_TIMEOUT_MS = 1_000;
+ private static final int TIMEOUT_MS = 2_000;
+ private static final int WAIT_MS = 1_000;
+
+ private BluetoothDevice mTestDevice;
+ @Mock private AdapterService mAdapterService;
+ @Mock private BatteryService mBatteryService;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ //TODO: check flag or override it
+ Assume.assumeTrue("Ignore test when Battery service is not enabled",
+ mTargetContext.getResources().getBoolean(R.bool.profile_supported_battery));
+ TestUtils.setAdapterService(mAdapterService);
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ // Get a device for testing
+ mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+
+ // Set up thread and looper
+ mHandlerThread = new HandlerThread("BatteryStateMachineTestHandlerThread");
+ mHandlerThread.start();
+ mBatteryStateMachine = new StubBatteryStateMachine(mTestDevice,
+ mBatteryService, mHandlerThread.getLooper());
+ // Override the timeout value to speed up the test
+ mBatteryStateMachine.sConnectTimeoutMs = CONNECTION_TIMEOUT_MS;
+ mBatteryStateMachine.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_battery)) {
+ return;
+ }
+ mBatteryStateMachine.doQuit();
+ mHandlerThread.quit();
+ TestUtils.clearAdapterService(mAdapterService);
+ reset(mBatteryService);
+ }
+
+ /**
+ * Test that default state is disconnected
+ */
+ @Test
+ public void testDefaultDisconnectedState() {
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mBatteryStateMachine.getConnectionState());
+ }
+
+ /**
+ * Allow/disallow connection to any device.
+ *
+ * @param allow if true, connection is allowed
+ */
+ private void allowConnection(boolean allow) {
+ when(mBatteryService.canConnect(any(BluetoothDevice.class))).thenReturn(allow);
+ }
+
+ private void allowConnectGatt(boolean allow) {
+ mBatteryStateMachine.mShouldAllowGatt = allow;
+ }
+
+ /**
+ * Test that an incoming connection with policy forbidding connection is rejected
+ */
+ @Test
+ public void testOkToConnectFails() {
+ allowConnection(false);
+ allowConnectGatt(true);
+
+ // Inject an event for when incoming connection is requested
+ mBatteryStateMachine.sendMessage(BatteryStateMachine.CONNECT);
+
+ verify(mBatteryService, after(WAIT_MS).never())
+ .handleConnectionStateChanged(any(BatteryStateMachine.class), anyInt(), anyInt());
+
+ // Check that we are in Disconnected state
+ Assert.assertThat(mBatteryStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BatteryStateMachine.Disconnected.class));
+ }
+
+ @Test
+ public void testFailToConnectGatt() {
+ allowConnection(true);
+ allowConnectGatt(false);
+
+ // Inject an event for when incoming connection is requested
+ mBatteryStateMachine.sendMessage(BatteryStateMachine.CONNECT);
+
+ verify(mBatteryService, after(WAIT_MS).never())
+ .handleConnectionStateChanged(any(BatteryStateMachine.class), anyInt(), anyInt());
+
+ // Check that we are in Disconnected state
+ Assert.assertThat(mBatteryStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BatteryStateMachine.Disconnected.class));
+ }
+
+ @Test
+ public void testSuccessfullyConnected() {
+ allowConnection(true);
+ allowConnectGatt(true);
+
+ // Inject an event for when incoming connection is requested
+ mBatteryStateMachine.sendMessage(BatteryStateMachine.CONNECT);
+
+ verify(mBatteryService, timeout(TIMEOUT_MS))
+ .handleConnectionStateChanged(any(BatteryStateMachine.class),
+ eq(BluetoothProfile.STATE_DISCONNECTED),
+ eq(BluetoothProfile.STATE_CONNECTING));
+
+ Assert.assertThat(mBatteryStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BatteryStateMachine.Connecting.class));
+
+ assertNotNull(mBatteryStateMachine.mGattCallback);
+ mBatteryStateMachine.notifyConnectionStateChanged(
+ GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);
+
+ verify(mBatteryService, timeout(TIMEOUT_MS))
+ .handleConnectionStateChanged(any(BatteryStateMachine.class),
+ eq(BluetoothProfile.STATE_CONNECTING),
+ eq(BluetoothProfile.STATE_CONNECTED));
+
+ Assert.assertThat(mBatteryStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BatteryStateMachine.Connected.class));
+ }
+
+ @Test
+ public void testConnectGattTimeout() {
+ allowConnection(true);
+ allowConnectGatt(true);
+
+ // Inject an event for when incoming connection is requested
+ mBatteryStateMachine.sendMessage(BatteryStateMachine.CONNECT);
+
+ verify(mBatteryService, timeout(TIMEOUT_MS))
+ .handleConnectionStateChanged(any(BatteryStateMachine.class),
+ eq(BluetoothProfile.STATE_DISCONNECTED),
+ eq(BluetoothProfile.STATE_CONNECTING));
+
+ Assert.assertThat(mBatteryStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BatteryStateMachine.Connecting.class));
+
+ verify(mBatteryService, timeout(TIMEOUT_MS))
+ .handleConnectionStateChanged(any(BatteryStateMachine.class),
+ eq(BluetoothProfile.STATE_CONNECTING),
+ eq(BluetoothProfile.STATE_DISCONNECTED));
+
+ Assert.assertThat(mBatteryStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BatteryStateMachine.Disconnected.class));
+ }
+
+ // It simulates GATT connection for testing.
+ public class StubBatteryStateMachine extends BatteryStateMachine {
+ boolean mShouldAllowGatt = true;
+
+ StubBatteryStateMachine(BluetoothDevice device, BatteryService service, Looper looper) {
+ super(device, service, looper);
+ }
+
+ @Override
+ public boolean connectGatt() {
+ mGattCallback = new GattCallback();
+ return mShouldAllowGatt;
+ }
+
+ public void notifyConnectionStateChanged(int status, int newState) {
+ if (mGattCallback != null) {
+ mGattCallback.onConnectionStateChange(mBluetoothGatt, status, newState);
+ }
+ }
+ }
+}
+
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index 57eed65e5f..eb046d832e 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -1128,6 +1128,29 @@ public final class DatabaseManagerTest {
}
}
+ @Test
+ public void testDatabaseMigration_111_112() throws IOException {
+ String testString = "TEST STRING";
+ // Create a database with version 109
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 111);
+ // insert a device to the database
+ ContentValues device = new ContentValues();
+ device.put("address", TEST_BT_ADDR);
+ device.put("migrated", false);
+ assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+ CoreMatchers.not(-1));
+ // Migrate database from 111 to 112
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 112, true,
+ MetadataDatabase.MIGRATION_111_112);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+ assertHasColumn(cursor, "battery_connection_policy", true);
+ while (cursor.moveToNext()) {
+ // Check the new columns was added with default value
+ assertColumnIntData(cursor, "battery_connection_policy", 100);
+ }
+ }
+
/**
* Helper function to check whether the database has the expected column
*/
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/112.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/112.json
new file mode 100644
index 0000000000..a0228ea0f7
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/112.json
@@ -0,0 +1,322 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 112,
+ "identityHash": "43c4197a0566aabd64121fba2d08407c",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "last_active_time",
+ "columnName": "last_active_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "is_active_a2dp_device",
+ "columnName": "is_active_a2dp_device",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+ "columnName": "a2dp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+ "columnName": "a2dp_sink_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+ "columnName": "hfp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+ "columnName": "hfp_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+ "columnName": "hid_host_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+ "columnName": "pan_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+ "columnName": "pbap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+ "columnName": "pbap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_connection_policy",
+ "columnName": "map_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+ "columnName": "sap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+ "columnName": "hearing_aid_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+ "columnName": "hap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+ "columnName": "map_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+ "columnName": "le_audio_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+ "columnName": "volume_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+ "columnName": "csip_set_coordinator_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+ "columnName": "le_call_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+ "columnName": "bass_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+ "columnName": "battery_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_untethered_headset",
+ "columnName": "is_untethered_headset",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_icon",
+ "columnName": "untethered_left_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_icon",
+ "columnName": "untethered_right_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_icon",
+ "columnName": "untethered_case_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_battery",
+ "columnName": "untethered_left_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_battery",
+ "columnName": "untethered_right_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_battery",
+ "columnName": "untethered_case_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_charging",
+ "columnName": "untethered_left_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_charging",
+ "columnName": "untethered_right_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_charging",
+ "columnName": "untethered_case_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.device_type",
+ "columnName": "device_type",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_battery",
+ "columnName": "main_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_charging",
+ "columnName": "main_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_low_battery_threshold",
+ "columnName": "main_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+ "columnName": "untethered_left_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+ "columnName": "untethered_right_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+ "columnName": "untethered_case_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "address"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '43c4197a0566aabd64121fba2d08407c')"
+ ]
+ }
+} \ No newline at end of file
diff --git a/framework/java/android/bluetooth/BluetoothProfile.java b/framework/java/android/bluetooth/BluetoothProfile.java
index 190cbf7f9c..78d0e136cf 100644
--- a/framework/java/android/bluetooth/BluetoothProfile.java
+++ b/framework/java/android/bluetooth/BluetoothProfile.java
@@ -267,12 +267,19 @@ public interface BluetoothProfile {
int LE_AUDIO_BROADCAST_ASSISTANT = 29;
/**
+ * Battery Service
+ *
+ * @hide
+ */
+ int BATTERY = 30;
+
+ /**
* Max profile ID. This value should be updated whenever a new profile is added to match
* the largest value assigned to a profile.
*
* @hide
*/
- int MAX_PROFILE_ID = 29;
+ int MAX_PROFILE_ID = 30;
/**
* Default priority for devices that we try to auto-connect to and
diff --git a/framework/java/android/bluetooth/BluetoothUuid.java b/framework/java/android/bluetooth/BluetoothUuid.java
index 7847718c5d..c77b981d35 100644
--- a/framework/java/android/bluetooth/BluetoothUuid.java
+++ b/framework/java/android/bluetooth/BluetoothUuid.java
@@ -197,6 +197,10 @@ public final class BluetoothUuid {
ParcelUuid.fromString("00001853-0000-1000-8000-00805F9B34FB");
/** @hide */
@NonNull
+ public static final ParcelUuid BATTERY =
+ ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
@SystemApi
public static final ParcelUuid BASS =
ParcelUuid.fromString("0000184F-0000-1000-8000-00805F9B34FB");
diff --git a/system/binder/Android.bp b/system/binder/Android.bp
index 72e11e7202..d82a4dc234 100644
--- a/system/binder/Android.bp
+++ b/system/binder/Android.bp
@@ -17,6 +17,7 @@ filegroup {
"android/bluetooth/IBluetoothA2dpSink.aidl",
"android/bluetooth/IBluetoothAvrcpController.aidl",
"android/bluetooth/IBluetoothAvrcpTarget.aidl",
+ "android/bluetooth/IBluetoothBattery.aidl",
"android/bluetooth/IBluetoothCallback.aidl",
"android/bluetooth/IBluetoothCsipSetCoordinator.aidl",
"android/bluetooth/IBluetoothCsipSetCoordinatorCallback.aidl",
diff --git a/system/binder/android/bluetooth/IBluetoothBattery.aidl b/system/binder/android/bluetooth/IBluetoothBattery.aidl
new file mode 100644
index 0000000000..dbb5cfcad5
--- /dev/null
+++ b/system/binder/android/bluetooth/IBluetoothBattery.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.AttributionSource;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+/**
+ * APIs for Bluetooth Battery service
+ *
+ * @hide
+ */
+oneway interface IBluetoothBattery {
+ /* Public API */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void connect(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void disconnect(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void getConnectedDevices(in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void getDevicesMatchingConnectionStates(in int[] states, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void getConnectionState(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
+ void setConnectionPolicy(in BluetoothDevice device, int connectionPolicy, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
+ void getConnectionPolicy(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+}
diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc
index 2b836bc31b..b54a275100 100644
--- a/system/btif/src/btif_dm.cc
+++ b/system/btif/src/btif_dm.cc
@@ -97,6 +97,7 @@ const Uuid UUID_LE_AUDIO = Uuid::FromString("184E");
const Uuid UUID_LE_MIDI = Uuid::FromString("03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
const Uuid UUID_HAS = Uuid::FromString("1854");
const Uuid UUID_BASS = Uuid::FromString("184F");
+const Uuid UUID_BATTERY = Uuid::FromString("180F");
const bool enable_address_consolidate = true; // TODO remove
#define COD_MASK 0x07FF
@@ -1336,7 +1337,8 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event,
static bool btif_is_interesting_le_service(bluetooth::Uuid uuid) {
return (uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID ||
uuid == UUID_VC || uuid == UUID_CSIS || uuid == UUID_LE_AUDIO ||
- uuid == UUID_LE_MIDI || uuid == UUID_HAS || uuid == UUID_BASS);
+ uuid == UUID_LE_MIDI || uuid == UUID_HAS || uuid == UUID_BASS ||
+ uuid == UUID_BATTERY);
}
/*******************************************************************************