blob: 7c16d1c8bfbf2f60fb500c64f3590587fb3542f0 [file] [log] [blame]
/*
* Copyright (C) 2012-2014 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.btservice;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_SCAN;
import android.annotation.RequiresPermission;
import android.app.admin.SecurityLog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProtoEnums;
import android.bluetooth.BluetoothSinkAudioPolicy;
import android.bluetooth.IBluetoothConnectionCallback;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.MacAddress;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.SystemProperties;
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;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
final class RemoteDevices {
private static final boolean DBG = false;
private static final String TAG = "BluetoothRemoteDevices";
// Maximum number of device properties to remember
private static final int MAX_DEVICE_QUEUE_SIZE = 200;
private BluetoothAdapter mAdapter;
private AdapterService mAdapterService;
private ArrayList<BluetoothDevice> mSdpTracker;
private final Object mObject = new Object();
private static final int UUID_INTENT_DELAY = 6000;
private static final int MESSAGE_UUID_INTENT = 1;
private static final String LOG_SOURCE_DIS = "DIS";
private final HashMap<String, DeviceProperties> mDevices;
private final HashMap<String, String> mDualDevicesMap;
private ArrayDeque<String> mDeviceQueue;
/**
* Bluetooth HFP v1.8 specifies the Battery Charge indicator of AG can take values from
* {@code 0} to {@code 5}, but it does not specify how to map the values back to percentages.
* The following mapping is used:
* - Level 0: 0%
* - Level 1: midpoint of 1-25%
* - Level 2: midpoint of 26-50%
* - Level 3: midpoint of 51-75%
* - Level 4: midpoint of 76-99%
* - Level 5: 100%
*/
private static final int HFP_BATTERY_CHARGE_INDICATOR_0 = 0;
private static final int HFP_BATTERY_CHARGE_INDICATOR_1 = 13;
private static final int HFP_BATTERY_CHARGE_INDICATOR_2 = 38;
private static final int HFP_BATTERY_CHARGE_INDICATOR_3 = 63;
private static final int HFP_BATTERY_CHARGE_INDICATOR_4 = 88;
private static final int HFP_BATTERY_CHARGE_INDICATOR_5 = 100;
private final Handler mHandler;
private class RemoteDevicesHandler extends Handler {
/**
* Handler must be created from an explicit looper to avoid threading ambiguity
* @param looper The looper that this handler should be executed on
*/
RemoteDevicesHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_UUID_INTENT:
BluetoothDevice device = (BluetoothDevice) msg.obj;
if (device != null) {
// SDP Sending delayed SDP UUID intent
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_SENDING_DELAYED_UUID, 1);
DeviceProperties prop = getDeviceProperties(device);
sendUuidIntent(device, prop);
} else {
// SDP Not sending delayed SDP UUID intent b/c device is not there
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_NOT_SENDING_DELAYED_UUID, 1);
}
break;
}
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED:
onHfIndicatorValueChanged(intent);
break;
case BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT:
onVendorSpecificHeadsetEvent(intent);
break;
case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
onHeadsetConnectionStateChanged(intent);
break;
case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED:
onHeadsetClientConnectionStateChanged(intent);
break;
case BluetoothHeadsetClient.ACTION_AG_EVENT:
onAgIndicatorValueChanged(intent);
break;
default:
Log.w(TAG, "Unhandled intent: " + intent);
break;
}
}
};
/**
* Predicate that tests if the given {@link BluetoothDevice} is well-known
* to be used for physical location.
*/
private final Predicate<BluetoothDevice> mLocationDenylistPredicate = (device) -> {
final MacAddress parsedAddress = MacAddress.fromString(device.getAddress());
if (mAdapterService.getLocationDenylistMac().test(parsedAddress.toByteArray())) {
Log.v(TAG, "Skipping device matching denylist: " + device);
return true;
}
final String name = Utils.getName(device);
if (mAdapterService.getLocationDenylistName().test(name)) {
Log.v(TAG, "Skipping name matching denylist: " + name);
return true;
}
return false;
};
RemoteDevices(AdapterService service, Looper looper) {
mAdapter = ((Context) service).getSystemService(BluetoothManager.class).getAdapter();
mAdapterService = service;
mSdpTracker = new ArrayList<BluetoothDevice>();
mDevices = new HashMap<String, DeviceProperties>();
mDualDevicesMap = new HashMap<String, String>();
mDeviceQueue = new ArrayDeque<>();
mHandler = new RemoteDevicesHandler(looper);
}
/**
* Init should be called before using this RemoteDevices object
*/
void init() {
IntentFilter filter = new IntentFilter();
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
filter.addAction(BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
+ BluetoothAssignedNumbers.PLANTRONICS);
filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
+ BluetoothAssignedNumbers.APPLE);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadsetClient.ACTION_AG_EVENT);
mAdapterService.registerReceiver(mReceiver, filter);
}
/**
* Clean up should be called when this object is no longer needed, must be called after init()
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
void cleanup() {
// Unregister receiver first, mAdapterService is never null
mAdapterService.unregisterReceiver(mReceiver);
reset();
}
/**
* Reset should be called when the state of this object needs to be cleared
* RemoteDevices is still usable after reset
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
void reset() {
if (mSdpTracker != null) {
mSdpTracker.clear();
}
synchronized (mDevices) {
if (mDevices != null) {
debugLog("reset(): Broadcasting ACL_DISCONNECTED");
mDevices.forEach((address, deviceProperties) -> {
BluetoothDevice bluetoothDevice = deviceProperties.getDevice();
debugLog("reset(): address=" + address + ", connected="
+ bluetoothDevice.isConnected());
if (bluetoothDevice.isConnected()) {
Intent intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bluetoothDevice);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
mAdapterService.sendBroadcast(intent, BLUETOOTH_CONNECT);
}
});
mDevices.clear();
}
}
if (mDualDevicesMap != null) {
mDualDevicesMap.clear();
}
if (mDeviceQueue != null) {
mDeviceQueue.clear();
}
}
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
DeviceProperties getDeviceProperties(BluetoothDevice device) {
synchronized (mDevices) {
String address = mDualDevicesMap.get(device.getAddress());
// If the device is not in the dual map, use its original address
if (address == null || mDevices.get(address) == null) {
address = device.getAddress();
}
return mDevices.get(address);
}
}
BluetoothDevice getDevice(byte[] address) {
String addressString = Utils.getAddressStringFromByte(address);
String deviceAddress = mDualDevicesMap.get(addressString);
// If the device is not in the dual map, use its original address
if (deviceAddress == null || mDevices.get(deviceAddress) == null) {
deviceAddress = addressString;
}
DeviceProperties prop = mDevices.get(deviceAddress);
if (prop != null) {
return prop.getDevice();
}
return null;
}
@VisibleForTesting
DeviceProperties addDeviceProperties(byte[] address) {
synchronized (mDevices) {
DeviceProperties prop = new DeviceProperties();
prop.setDevice(mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address)));
prop.setAddress(address);
String key = Utils.getAddressStringFromByte(address);
DeviceProperties pv = mDevices.put(key, prop);
if (pv == null) {
mDeviceQueue.offer(key);
if (mDeviceQueue.size() > MAX_DEVICE_QUEUE_SIZE) {
String deleteKey = mDeviceQueue.poll();
for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
if (device.getAddress().equals(deleteKey)) {
return prop;
}
}
debugLog("Removing device " + deleteKey + " from property map");
mDevices.remove(deleteKey);
}
}
return prop;
}
}
class DeviceProperties {
private String mName;
private byte[] mAddress;
private String mIdentityAddress;
private boolean mIsConsolidated = false;
private int mBluetoothClass = BluetoothClass.Device.Major.UNCATEGORIZED;
private int mBredrConnectionHandle = BluetoothDevice.ERROR;
private int mLeConnectionHandle = BluetoothDevice.ERROR;
private short mRssi;
private String mAlias;
private BluetoothDevice mDevice;
private boolean mIsBondingInitiatedLocally;
private int mBatteryLevelFromHfp = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
private int mBatteryLevelFromBatteryService = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
private boolean mIsCoordinatedSetMember;
private int mAshaCapability;
private int mAshaTruncatedHiSyncId;
private String mModelName;
@VisibleForTesting int mBondState;
@VisibleForTesting int mDeviceType;
@VisibleForTesting ParcelUuid[] mUuids;
private BluetoothSinkAudioPolicy mAudioPolicy;
DeviceProperties() {
mBondState = BluetoothDevice.BOND_NONE;
}
/**
* @return the mName
*/
String getName() {
synchronized (mObject) {
return mName;
}
}
/**
* @param name the mName to set
*/
void setName(String name) {
synchronized (mObject) {
this.mName = name;
}
}
/**
* @return the mIdentityAddress
*/
String getIdentityAddress() {
synchronized (mObject) {
return mIdentityAddress;
}
}
/**
* @param identityAddress the mIdentityAddress to set
*/
void setIdentityAddress(String identityAddress) {
synchronized (mObject) {
this.mIdentityAddress = identityAddress;
}
}
/**
* @return mIsConsolidated
*/
boolean isConsolidated() {
synchronized (mObject) {
return mIsConsolidated;
}
}
/**
* @param isConsolidated the mIsConsolidated to set
*/
void setIsConsolidated(boolean isConsolidated) {
synchronized (mObject) {
this.mIsConsolidated = isConsolidated;
}
}
/**
* @return the mClass
*/
int getBluetoothClass() {
synchronized (mObject) {
return mBluetoothClass;
}
}
/**
* @param bluetoothClass the mBluetoothClass to set
*/
void setBluetoothClass(int bluetoothClass) {
synchronized (mObject) {
this.mBluetoothClass = bluetoothClass;
}
}
/**
* @param transport the transport on which the connection exists
* @return the mConnectionHandle
*/
int getConnectionHandle(int transport) {
synchronized (mObject) {
if (transport == BluetoothDevice.TRANSPORT_BREDR) {
return mBredrConnectionHandle;
} else if (transport == BluetoothDevice.TRANSPORT_LE) {
return mLeConnectionHandle;
} else {
return BluetoothDevice.ERROR;
}
}
}
/**
* @param connectionHandle the connectionHandle to set
* @param transport the transport on which to set the handle
*/
void setConnectionHandle(int connectionHandle, int transport) {
synchronized (mObject) {
if (transport == BluetoothDevice.TRANSPORT_BREDR) {
mBredrConnectionHandle = connectionHandle;
} else if (transport == BluetoothDevice.TRANSPORT_LE) {
mLeConnectionHandle = connectionHandle;
} else {
errorLog("setConnectionHandle() unexpected transport value " + transport);
}
}
}
/**
* @return the mUuids
*/
ParcelUuid[] getUuids() {
synchronized (mObject) {
return mUuids;
}
}
/**
* @param uuids the mUuids to set
*/
void setUuids(ParcelUuid[] uuids) {
synchronized (mObject) {
this.mUuids = uuids;
}
}
/**
* @return the mAddress
*/
byte[] getAddress() {
synchronized (mObject) {
return mAddress;
}
}
/**
* @param address the mAddress to set
*/
void setAddress(byte[] address) {
synchronized (mObject) {
this.mAddress = address;
}
}
/**
* @return the mDevice
*/
BluetoothDevice getDevice() {
synchronized (mObject) {
return mDevice;
}
}
/**
* @param device the mDevice to set
*/
void setDevice(BluetoothDevice device) {
synchronized (mObject) {
this.mDevice = device;
}
}
/**
* @return mRssi
*/
short getRssi() {
synchronized (mObject) {
return mRssi;
}
}
/**
* @param rssi the mRssi to set
*/
void setRssi(short rssi) {
synchronized (mObject) {
this.mRssi = rssi;
}
}
/**
* @return mDeviceType
*/
int getDeviceType() {
synchronized (mObject) {
return mDeviceType;
}
}
/**
* @param deviceType the mDeviceType to set
*/
void setDeviceType(int deviceType) {
synchronized (mObject) {
this.mDeviceType = deviceType;
}
}
/**
* @return the mAlias
*/
String getAlias() {
synchronized (mObject) {
return mAlias;
}
}
/**
* @param mAlias the mAlias to set
*/
void setAlias(BluetoothDevice device, String mAlias) {
synchronized (mObject) {
this.mAlias = mAlias;
mAdapterService.setDevicePropertyNative(mAddress,
AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME, mAlias.getBytes());
Intent intent = new Intent(BluetoothDevice.ACTION_ALIAS_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_NAME, mAlias);
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
}
}
/**
* @param newBondState the mBondState to set
*/
void setBondState(int newBondState) {
synchronized (mObject) {
this.mBondState = newBondState;
if (newBondState == BluetoothDevice.BOND_NONE) {
/* Clearing the Uuids local copy when the device is unpaired. If not cleared,
cachedBluetoothDevice issued a connect using the local cached copy of uuids,
without waiting for the ACTION_UUID intent.
This was resulting in multiple calls to connect().*/
mUuids = null;
mAlias = null;
}
}
}
/**
* @return the mBondState
*/
int getBondState() {
synchronized (mObject) {
return mBondState;
}
}
boolean isBonding() {
return getBondState() == BluetoothDevice.BOND_BONDING;
}
boolean isBondingOrBonded() {
return isBonding() || getBondState() == BluetoothDevice.BOND_BONDED;
}
/**
* @param isBondingInitiatedLocally wether bonding is initiated locally
*/
void setBondingInitiatedLocally(boolean isBondingInitiatedLocally) {
synchronized (mObject) {
this.mIsBondingInitiatedLocally = isBondingInitiatedLocally;
}
}
/**
* @return the isBondingInitiatedLocally
*/
boolean isBondingInitiatedLocally() {
synchronized (mObject) {
return mIsBondingInitiatedLocally;
}
}
/**
* @return mBatteryLevel
*/
int getBatteryLevel() {
synchronized (mObject) {
if (mBatteryLevelFromBatteryService != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
return mBatteryLevelFromBatteryService;
}
return mBatteryLevelFromHfp;
}
}
void setBatteryLevelFromHfp(int batteryLevel) {
synchronized (mObject) {
if (mBatteryLevelFromHfp == batteryLevel) {
return;
}
mBatteryLevelFromHfp = batteryLevel;
}
}
void setBatteryLevelFromBatteryService(int batteryLevel) {
synchronized (mObject) {
if (mBatteryLevelFromBatteryService == batteryLevel) {
return;
}
mBatteryLevelFromBatteryService = batteryLevel;
}
}
/**
* @return the mIsCoordinatedSetMember
*/
boolean isCoordinatedSetMember() {
synchronized (mObject) {
return mIsCoordinatedSetMember;
}
}
/**
* @param isCoordinatedSetMember the mIsCoordinatedSetMember to set
*/
void setIsCoordinatedSetMember(boolean isCoordinatedSetMember) {
if ((mAdapterService.getSupportedProfilesBitMask()
& (1 << BluetoothProfile.CSIP_SET_COORDINATOR))
== 0) {
debugLog("CSIP is not supported");
return;
}
synchronized (mObject) {
this.mIsCoordinatedSetMember = isCoordinatedSetMember;
}
}
/**
* @return the mAshaCapability
*/
int getAshaCapability() {
synchronized (mObject) {
return mAshaCapability;
}
}
void setAshaCapability(int ashaCapability) {
synchronized (mObject) {
this.mAshaCapability = ashaCapability;
}
}
/**
* @return the mAshaTruncatedHiSyncId
*/
int getAshaTruncatedHiSyncId() {
synchronized (mObject) {
return mAshaTruncatedHiSyncId;
}
}
void setAshaTruncatedHiSyncId(int ashaTruncatedHiSyncId) {
synchronized (mObject) {
this.mAshaTruncatedHiSyncId = ashaTruncatedHiSyncId;
}
}
public void setHfAudioPolicyForRemoteAg(BluetoothSinkAudioPolicy policies) {
mAudioPolicy = policies;
}
public BluetoothSinkAudioPolicy getHfAudioPolicyForRemoteAg() {
return mAudioPolicy;
}
public void setModelName(String modelName) {
mModelName = modelName;
}
/**
* @return the mModelName
*/
String getModelName() {
synchronized (mObject) {
return mModelName;
}
}
}
private void sendUuidIntent(BluetoothDevice device, DeviceProperties prop) {
// Send uuids within the stack before the broadcast is sent out
ParcelUuid[] uuids = prop == null ? null : prop.getUuids();
mAdapterService.sendUuidsInternal(device, uuids);
Intent intent = new Intent(BluetoothDevice.ACTION_UUID);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_UUID, uuids);
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
// SDP Sent UUID Intent here
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_SENT_UUID, 1);
//Remove the outstanding UUID request
mSdpTracker.remove(device);
}
/**
* When bonding is initiated to remote device that we have never seen, i.e Out Of Band pairing,
* we must add device first before setting it's properties. This is a helper method for doing
* that.
*/
void setBondingInitiatedLocally(byte[] address) {
DeviceProperties properties;
BluetoothDevice device = getDevice(address);
if (device == null) {
properties = addDeviceProperties(address);
} else {
properties = getDeviceProperties(device);
}
properties.setBondingInitiatedLocally(true);
}
/**
* Update battery level in device properties
*
* @param device The remote device to be updated
* @param batteryLevel Battery level Indicator between 0-100, {@link
* BluetoothDevice#BATTERY_LEVEL_UNKNOWN} is error
* @param isBas true if the battery level is from the battery service
*/
@VisibleForTesting
void updateBatteryLevel(BluetoothDevice device, int batteryLevel, boolean isBas) {
if (device == null || batteryLevel < 0 || batteryLevel > 100) {
warnLog(
"Invalid parameters device="
+ String.valueOf(device == null)
+ ", batteryLevel="
+ String.valueOf(batteryLevel));
return;
}
DeviceProperties deviceProperties = getDeviceProperties(device);
if (deviceProperties == null) {
deviceProperties = addDeviceProperties(Utils.getByteAddress(device));
}
int prevBatteryLevel = deviceProperties.getBatteryLevel();
if (isBas) {
deviceProperties.setBatteryLevelFromBatteryService(batteryLevel);
} else {
deviceProperties.setBatteryLevelFromHfp(batteryLevel);
}
int newBatteryLevel = deviceProperties.getBatteryLevel();
if (prevBatteryLevel == newBatteryLevel) {
debugLog(
"Same battery level for device "
+ device
+ " received "
+ String.valueOf(batteryLevel)
+ "%");
return;
}
sendBatteryLevelChangedBroadcast(device, newBatteryLevel);
Log.d(TAG, "Updated device " + device + " battery level to " + newBatteryLevel + "%");
}
/**
* Reset battery level property to {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} for a device
*
* @param device device whose battery level property needs to be reset
*/
@VisibleForTesting
void resetBatteryLevel(BluetoothDevice device, boolean isBas) {
if (device == null) {
warnLog("Device is null");
return;
}
DeviceProperties deviceProperties = getDeviceProperties(device);
if (deviceProperties == null) {
return;
}
int prevBatteryLevel = deviceProperties.getBatteryLevel();
if (isBas) {
deviceProperties.setBatteryLevelFromBatteryService(
BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
} else {
deviceProperties.setBatteryLevelFromHfp(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
}
int newBatteryLevel = deviceProperties.getBatteryLevel();
if (prevBatteryLevel == newBatteryLevel) {
debugLog("Battery level was not changed due to reset, device=" + device);
return;
}
sendBatteryLevelChangedBroadcast(device, newBatteryLevel);
Log.d(TAG, "Updated device " + device + " battery level to " + newBatteryLevel + "%");
}
private void sendBatteryLevelChangedBroadcast(BluetoothDevice device, int batteryLevel) {
Intent intent = new Intent(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, batteryLevel);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
}
/**
* Converts HFP's Battery Charge indicator values of {@code 0 -- 5} to an integer percentage.
*/
@VisibleForTesting
static int batteryChargeIndicatorToPercentge(int indicator) {
int percent;
switch (indicator) {
case 5:
percent = HFP_BATTERY_CHARGE_INDICATOR_5;
break;
case 4:
percent = HFP_BATTERY_CHARGE_INDICATOR_4;
break;
case 3:
percent = HFP_BATTERY_CHARGE_INDICATOR_3;
break;
case 2:
percent = HFP_BATTERY_CHARGE_INDICATOR_2;
break;
case 1:
percent = HFP_BATTERY_CHARGE_INDICATOR_1;
break;
case 0:
percent = HFP_BATTERY_CHARGE_INDICATOR_0;
break;
default:
percent = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
Log.d(TAG, "Battery charge indicator: " + indicator + "; converted to: " + percent + "%");
return percent;
}
private static boolean areUuidsEqual(ParcelUuid[] uuids1, ParcelUuid[] uuids2) {
final int length1 = uuids1 == null ? 0 : uuids1.length;
final int length2 = uuids2 == null ? 0 : uuids2.length;
if (length1 != length2) {
return false;
}
Set<ParcelUuid> set = new HashSet<>();
for (int i = 0; i < length1; ++i) {
set.add(uuids1[i]);
}
for (int i = 0; i < length2; ++i) {
set.remove(uuids2[i]);
}
return set.isEmpty();
}
void devicePropertyChangedCallback(byte[] address, int[] types, byte[][] values) {
Intent intent;
byte[] val;
int type;
BluetoothDevice bdDevice = getDevice(address);
DeviceProperties deviceProperties;
if (bdDevice == null) {
debugLog("Added new device property");
deviceProperties = addDeviceProperties(address);
bdDevice = getDevice(address);
} else {
deviceProperties = getDeviceProperties(bdDevice);
}
if (types.length <= 0) {
errorLog("No properties to update");
return;
}
for (int j = 0; j < types.length; j++) {
type = types[j];
val = values[j];
if (val.length > 0) {
synchronized (mObject) {
debugLog("Property type: " + type);
switch (type) {
case AbstractionLayer.BT_PROPERTY_BDNAME:
final String newName = new String(val);
if (newName.equals(deviceProperties.getName())) {
debugLog("Skip name update for " + bdDevice);
break;
}
deviceProperties.setName(newName);
intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProperties.getName());
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
debugLog("Remote device name is: " + deviceProperties.getName());
break;
case AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME:
deviceProperties.setAlias(bdDevice, new String(val));
debugLog("Remote device alias is: " + deviceProperties.getAlias());
break;
case AbstractionLayer.BT_PROPERTY_BDADDR:
deviceProperties.setAddress(val);
debugLog(
"Remote Address is:"
+ Utils.getRedactedAddressStringFromByte(val));
break;
case AbstractionLayer.BT_PROPERTY_CLASS_OF_DEVICE:
final int newBluetoothClass = Utils.byteArrayToInt(val);
if (newBluetoothClass == deviceProperties.getBluetoothClass()) {
debugLog("Skip class update for " + bdDevice);
break;
}
deviceProperties.setBluetoothClass(newBluetoothClass);
intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
intent.putExtra(BluetoothDevice.EXTRA_CLASS,
new BluetoothClass(deviceProperties.getBluetoothClass()));
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
debugLog("Remote class is:" + newBluetoothClass);
break;
case AbstractionLayer.BT_PROPERTY_UUIDS:
final ParcelUuid[] newUuids = Utils.byteArrayToUuid(val);
if (areUuidsEqual(newUuids, deviceProperties.getUuids())) {
// SDP Skip adding UUIDs to property cache if equal
debugLog("Skip uuids update for " + bdDevice.getAddress());
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_UUIDS_EQUAL_SKIP, 1);
break;
}
deviceProperties.setUuids(newUuids);
if (mAdapterService.getState() == BluetoothAdapter.STATE_ON) {
// SDP Adding UUIDs to property cache and sending intent
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_ADD_UUID_WITH_INTENT, 1);
mAdapterService.deviceUuidUpdated(bdDevice);
sendUuidIntent(bdDevice, deviceProperties);
} else if (mAdapterService.getState()
== BluetoothAdapter.STATE_BLE_ON) {
// SDP Adding UUIDs to property cache but with no intent
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_ADD_UUID_WITH_NO_INTENT, 1);
mAdapterService.deviceUuidUpdated(bdDevice);
} else {
// SDP Silently dropping UUIDs and with no intent
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_DROP_UUID, 1);
}
break;
case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
if (deviceProperties.isConsolidated()) {
break;
}
// The device type from hal layer, defined in bluetooth.h,
// matches the type defined in BluetoothDevice.java
deviceProperties.setDeviceType(Utils.byteArrayToInt(val));
break;
case AbstractionLayer.BT_PROPERTY_REMOTE_RSSI:
// RSSI from hal is in one byte
deviceProperties.setRssi(val[0]);
break;
case AbstractionLayer.BT_PROPERTY_REMOTE_IS_COORDINATED_SET_MEMBER:
deviceProperties.setIsCoordinatedSetMember(val[0] != 0);
break;
case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_CAPABILITY:
deviceProperties.setAshaCapability(val[0]);
break;
case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID:
deviceProperties.setAshaTruncatedHiSyncId(val[0]);
break;
case AbstractionLayer.BT_PROPERTY_REMOTE_MODEL_NUM:
final String modelName = new String(val);
debugLog("Remote device model name: " + modelName);
deviceProperties.setModelName(modelName);
BluetoothStatsLog.write(
BluetoothStatsLog.BLUETOOTH_DEVICE_INFO_REPORTED,
mAdapterService.obfuscateAddress(bdDevice),
BluetoothProtoEnums.DEVICE_INFO_INTERNAL, LOG_SOURCE_DIS, null,
modelName, null, null, mAdapterService.getMetricId(bdDevice),
bdDevice.getAddressType(), 0, 0, 0);
break;
}
}
}
}
}
void deviceFoundCallback(byte[] address) {
// The device properties are already registered - we can send the intent
// now
BluetoothDevice device = getDevice(address);
debugLog("deviceFoundCallback: Remote Address is:" + device);
DeviceProperties deviceProp = getDeviceProperties(device);
if (deviceProp == null) {
errorLog("Device Properties is null for Device:" + device);
return;
}
boolean restrict_device_found =
SystemProperties.getBoolean("bluetooth.restrict_discovered_device.enabled", false);
if (restrict_device_found && (deviceProp.mName == null || deviceProp.mName.isEmpty())) {
debugLog("Device name is null or empty: " + device);
return;
}
Intent intent = new Intent(BluetoothDevice.ACTION_FOUND);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_CLASS,
new BluetoothClass(deviceProp.getBluetoothClass()));
intent.putExtra(BluetoothDevice.EXTRA_RSSI, deviceProp.getRssi());
intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProp.getName());
intent.putExtra(BluetoothDevice.EXTRA_IS_COORDINATED_SET_MEMBER,
deviceProp.isCoordinatedSetMember());
final ArrayList<DiscoveringPackage> packages = mAdapterService.getDiscoveringPackages();
synchronized (packages) {
for (DiscoveringPackage pkg : packages) {
if (pkg.hasDisavowedLocation()) {
if (mLocationDenylistPredicate.test(device)) {
continue;
}
}
intent.setPackage(pkg.getPackageName());
if (pkg.getPermission() != null) {
mAdapterService.sendBroadcastMultiplePermissions(intent,
new String[] { BLUETOOTH_SCAN, pkg.getPermission() },
Utils.getTempBroadcastOptions());
} else {
mAdapterService.sendBroadcastMultiplePermissions(intent,
new String[] { BLUETOOTH_SCAN },
Utils.getTempBroadcastOptions());
}
}
}
}
void addressConsolidateCallback(byte[] mainAddress, byte[] secondaryAddress) {
BluetoothDevice device = getDevice(mainAddress);
if (device == null) {
errorLog("addressConsolidateCallback: device is NULL, address="
+ Utils.getRedactedAddressStringFromByte(mainAddress)
+ ", secondaryAddress="
+ Utils.getRedactedAddressStringFromByte(secondaryAddress));
return;
}
Log.d(TAG, "addressConsolidateCallback device: " + device + ", secondaryAddress:"
+ Utils.getRedactedAddressStringFromByte(secondaryAddress));
DeviceProperties deviceProperties = getDeviceProperties(device);
deviceProperties.setIsConsolidated(true);
deviceProperties.setDeviceType(BluetoothDevice.DEVICE_TYPE_DUAL);
deviceProperties.setIdentityAddress(Utils.getAddressStringFromByte(secondaryAddress));
mDualDevicesMap.put(deviceProperties.getIdentityAddress(), Utils.getAddressStringFromByte(mainAddress));
}
/**
* Callback to associate an LE-only device's RPA with its identity address
*
* @param mainAddress the device's RPA
* @param secondaryAddress the device's identity address
*/
void leAddressAssociateCallback(byte[] mainAddress, byte[] secondaryAddress) {
BluetoothDevice device = getDevice(mainAddress);
if (device == null) {
errorLog("leAddressAssociateCallback: device is NULL, address="
+ Utils.getRedactedAddressStringFromByte(mainAddress)
+ ", secondaryAddress="
+ Utils.getRedactedAddressStringFromByte(secondaryAddress));
return;
}
Log.d(TAG, "leAddressAssociateCallback device: " + device + ", secondaryAddress:"
+ Utils.getRedactedAddressStringFromByte(secondaryAddress));
DeviceProperties deviceProperties = getDeviceProperties(device);
deviceProperties.mIdentityAddress = Utils.getAddressStringFromByte(secondaryAddress);
}
@RequiresPermission(allOf = {
android.Manifest.permission.BLUETOOTH_CONNECT,
android.Manifest.permission.BLUETOOTH_PRIVILEGED,
})
void aclStateChangeCallback(int status, byte[] address, int newState,
int transportLinkType, int hciReason, int handle) {
if (status != AbstractionLayer.BT_STATUS_SUCCESS) {
debugLog("aclStateChangeCallback status is " + status + ", skipping");
return;
}
BluetoothDevice device = getDevice(address);
if (device == null) {
warnLog("aclStateChangeCallback: device is NULL, address="
+ Utils.getRedactedAddressStringFromByte(address)
+ ", newState=" + newState);
addDeviceProperties(address);
device = Objects.requireNonNull(getDevice(address));
}
DeviceProperties deviceProperties = getDeviceProperties(device);
int state = mAdapterService.getState();
Intent intent = null;
if (newState == AbstractionLayer.BT_ACL_STATE_CONNECTED) {
deviceProperties.setConnectionHandle(handle, transportLinkType);
if (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_TURNING_ON) {
intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED);
intent.putExtra(BluetoothDevice.EXTRA_TRANSPORT, transportLinkType);
} else if (state == BluetoothAdapter.STATE_BLE_ON
|| state == BluetoothAdapter.STATE_BLE_TURNING_ON) {
intent = new Intent(BluetoothAdapter.ACTION_BLE_ACL_CONNECTED);
}
BatteryService batteryService = BatteryService.getBatteryService();
if (batteryService != null && transportLinkType == BluetoothDevice.TRANSPORT_LE) {
batteryService.connectIfPossible(device);
}
SecurityLog.writeEvent(SecurityLog.TAG_BLUETOOTH_CONNECTION,
Utils.getLoggableAddress(device), /* success */ 1, /* reason */ "");
debugLog(
"aclStateChangeCallback: Adapter State: " + BluetoothAdapter.nameForState(state)
+ " Connected: " + device);
} else {
deviceProperties.setConnectionHandle(BluetoothDevice.ERROR, transportLinkType);
if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
// Send PAIRING_CANCEL intent to dismiss any dialog requesting bonding.
intent = new Intent(BluetoothDevice.ACTION_PAIRING_CANCEL);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.setPackage(SystemProperties.get(
Utils.PAIRING_UI_PROPERTY,
mAdapterService.getString(R.string.pairing_ui_package)));
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
} else if (device.getBondState() == BluetoothDevice.BOND_NONE) {
String key = Utils.getAddressStringFromByte(address);
mDevices.remove(key);
}
if (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_TURNING_OFF) {
intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED);
intent.putExtra(BluetoothDevice.EXTRA_TRANSPORT, transportLinkType);
} else if (state == BluetoothAdapter.STATE_BLE_ON
|| state == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
intent = new Intent(BluetoothAdapter.ACTION_BLE_ACL_DISCONNECTED);
}
// Reset battery level on complete disconnection
if (mAdapterService.getConnectionState(device) == 0) {
BatteryService batteryService = BatteryService.getBatteryService();
if (batteryService != null
&& batteryService.getConnectionState(device)
!= BluetoothProfile.STATE_DISCONNECTED
&& transportLinkType == BluetoothDevice.TRANSPORT_LE) {
batteryService.disconnect(device);
}
resetBatteryLevel(device, /*isBas=*/ true);
}
if (mAdapterService.isAllProfilesUnknown(device)) {
DeviceProperties deviceProp = getDeviceProperties(device);
if (deviceProp != null) {
deviceProp.setBondingInitiatedLocally(false);
}
}
SecurityLog.writeEvent(SecurityLog.TAG_BLUETOOTH_DISCONNECTION,
Utils.getLoggableAddress(device),
BluetoothAdapter.BluetoothConnectionCallback.disconnectReasonToString(
AdapterService.hciToAndroidDisconnectReason(hciReason)));
debugLog(
"aclStateChangeCallback: Adapter State: " + BluetoothAdapter.nameForState(state)
+ " Disconnected: " + device
+ " transportLinkType: " + transportLinkType
+ " hciReason: " + hciReason);
}
int connectionState = newState == AbstractionLayer.BT_ACL_STATE_CONNECTED
? BluetoothAdapter.STATE_CONNECTED : BluetoothAdapter.STATE_DISCONNECTED;
int metricId = mAdapterService.getMetricId(device);
BluetoothStatsLog.write(
BluetoothStatsLog.BLUETOOTH_ACL_CONNECTION_STATE_CHANGED,
mAdapterService.obfuscateAddress(device),
connectionState,
metricId,
transportLinkType);
BluetoothClass deviceClass = device.getBluetoothClass();
int classOfDevice = deviceClass == null ? 0 : deviceClass.getClassOfDevice();
BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_CLASS_OF_DEVICE_REPORTED,
mAdapterService.obfuscateAddress(device), classOfDevice, metricId);
if (intent != null) {
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
Utils.sendBroadcast(mAdapterService, intent, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
synchronized (mAdapterService.getBluetoothConnectionCallbacks()) {
Set<IBluetoothConnectionCallback> bluetoothConnectionCallbacks =
mAdapterService.getBluetoothConnectionCallbacks();
for (IBluetoothConnectionCallback callback : bluetoothConnectionCallbacks) {
try {
if (connectionState == BluetoothAdapter.STATE_CONNECTED) {
callback.onDeviceConnected(device);
} else {
callback.onDeviceDisconnected(device,
AdapterService.hciToAndroidDisconnectReason(hciReason));
}
} catch (RemoteException ex) {
Log.e(TAG, "RemoteException in calling IBluetoothConnectionCallback");
}
}
}
} else {
Log.e(TAG, "aclStateChangeCallback intent is null. deviceBondState: "
+ device.getBondState());
}
}
void fetchUuids(BluetoothDevice device, int transport) {
if (mSdpTracker.contains(device)) {
// SDP Skip fetch UUIDs if cached
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_FETCH_UUID_SKIP_ALREADY_CACHED, 1);
return;
}
// If no UUIDs are cached and the device is bonding, wait for SDP after the device is bonded
DeviceProperties deviceProperties = getDeviceProperties(device);
if (deviceProperties != null && deviceProperties.isBonding()
&& getDeviceProperties(device).getUuids() == null) {
// SDP Skip fetch UUIDs due to bonding
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_FETCH_UUID_SKIP_ALREADY_BONDED, 1);
return;
}
mSdpTracker.add(device);
Message message = mHandler.obtainMessage(MESSAGE_UUID_INTENT);
message.obj = device;
mHandler.sendMessageDelayed(message, UUID_INTENT_DELAY);
// Uses cached UUIDs if we are bonding. If not, we fetch the UUIDs with SDP.
if (deviceProperties == null || !deviceProperties.isBonding()) {
// SDP Invoked native code to spin up SDP cycle
mAdapterService.getRemoteServicesNative(Utils.getBytesFromAddress(device.getAddress()),
transport);
MetricsLogger.getInstance().cacheCount(
BluetoothProtoEnums.SDP_INVOKE_SDP_CYCLE, 1);
}
}
void updateUuids(BluetoothDevice device) {
Message message = mHandler.obtainMessage(MESSAGE_UUID_INTENT);
message.obj = device;
mHandler.sendMessage(message);
}
/**
* Handles headset connection state change event
* @param intent must be {@link BluetoothHeadset#ACTION_CONNECTION_STATE_CHANGED} intent
*/
@VisibleForTesting
void onHeadsetConnectionStateChanged(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
Log.e(TAG, "onHeadsetConnectionStateChanged() remote device is null");
return;
}
if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED)
== BluetoothProfile.STATE_DISCONNECTED
&& !hasBatteryService(device)) {
resetBatteryLevel(device, /*isBas=*/ false);
}
}
@VisibleForTesting
void onHfIndicatorValueChanged(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
Log.e(TAG, "onHfIndicatorValueChanged() remote device is null");
return;
}
int indicatorId = intent.getIntExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, -1);
int indicatorValue = intent.getIntExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, -1);
if (indicatorId == HeadsetHalConstants.HF_INDICATOR_BATTERY_LEVEL_STATUS) {
updateBatteryLevel(device, indicatorValue, /*isBas=*/ false);
}
}
/**
* Handle {@link BluetoothHeadset#ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intent
* @param intent must be {@link BluetoothHeadset#ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intent
*/
@VisibleForTesting
void onVendorSpecificHeadsetEvent(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
Log.e(TAG, "onVendorSpecificHeadsetEvent() remote device is null");
return;
}
String cmd =
intent.getStringExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD);
if (cmd == null) {
Log.e(TAG, "onVendorSpecificHeadsetEvent() command is null");
return;
}
int cmdType =
intent.getIntExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE,
-1);
// Only process set command
if (cmdType != BluetoothHeadset.AT_CMD_TYPE_SET) {
debugLog("onVendorSpecificHeadsetEvent() only SET command is processed");
return;
}
Object[] args = (Object[]) intent.getExtras()
.get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
if (args == null) {
Log.e(TAG, "onVendorSpecificHeadsetEvent() arguments are null");
return;
}
int batteryPercent = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
switch (cmd) {
case BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT:
batteryPercent = getBatteryLevelFromXEventVsc(args);
break;
case BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV:
batteryPercent = getBatteryLevelFromAppleBatteryVsc(args);
break;
}
if (batteryPercent != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
updateBatteryLevel(device, batteryPercent, /*isBas=*/ false);
infoLog("Updated device " + device + " battery level to " + String.valueOf(
batteryPercent) + "%");
}
}
/**
* Parse
* AT+IPHONEACCEV=[NumberOfIndicators],[IndicatorType],[IndicatorValue]
* vendor specific event
* @param args Array of arguments on the right side of assignment
* @return Battery level in percents, [0-100], {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
* when there is an error parsing the arguments
*/
@VisibleForTesting
static int getBatteryLevelFromAppleBatteryVsc(Object[] args) {
if (args.length == 0) {
Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() empty arguments");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
int numKvPair;
if (args[0] instanceof Integer) {
numKvPair = (Integer) args[0];
} else {
Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() error parsing number of arguments");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
if (args.length != (numKvPair * 2 + 1)) {
Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() number of arguments does not match");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
int indicatorType;
int indicatorValue = -1;
for (int i = 0; i < numKvPair; ++i) {
Object indicatorTypeObj = args[2 * i + 1];
if (indicatorTypeObj instanceof Integer) {
indicatorType = (Integer) indicatorTypeObj;
} else {
Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() error parsing indicator type");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
if (indicatorType
!= BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL) {
continue;
}
Object indicatorValueObj = args[2 * i + 2];
if (indicatorValueObj instanceof Integer) {
indicatorValue = (Integer) indicatorValueObj;
} else {
Log.w(TAG, "getBatteryLevelFromAppleBatteryVsc() error parsing indicator value");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
break;
}
return (indicatorValue < 0 || indicatorValue > 9) ? BluetoothDevice.BATTERY_LEVEL_UNKNOWN
: (indicatorValue + 1) * 10;
}
/**
* Parse
* AT+XEVENT=BATTERY,[Level],[NumberOfLevel],[MinutesOfTalk],[IsCharging]
* vendor specific event
* @param args Array of arguments on the right side of SET command
* @return Battery level in percents, [0-100], {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
* when there is an error parsing the arguments
*/
@VisibleForTesting
static int getBatteryLevelFromXEventVsc(Object[] args) {
if (args.length == 0) {
Log.w(TAG, "getBatteryLevelFromXEventVsc() empty arguments");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
Object eventNameObj = args[0];
if (!(eventNameObj instanceof String)) {
Log.w(TAG, "getBatteryLevelFromXEventVsc() error parsing event name");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
String eventName = (String) eventNameObj;
if (!eventName.equals(
BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL)) {
infoLog("getBatteryLevelFromXEventVsc() skip none BATTERY event: " + eventName);
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
if (args.length != 5) {
Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong battery level event length: "
+ String.valueOf(args.length));
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer)) {
Log.w(TAG, "getBatteryLevelFromXEventVsc() error parsing event values");
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
int batteryLevel = (Integer) args[1];
int numberOfLevels = (Integer) args[2];
if (batteryLevel < 0 || numberOfLevels <= 1 || batteryLevel > numberOfLevels) {
Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong event value, batteryLevel="
+ String.valueOf(batteryLevel) + ", numberOfLevels=" + String.valueOf(
numberOfLevels));
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
return batteryLevel * 100 / (numberOfLevels - 1);
}
@VisibleForTesting
boolean hasBatteryService(BluetoothDevice device) {
BatteryService batteryService = BatteryService.getBatteryService();
return batteryService != null
&& batteryService.getConnectionState(device) == BluetoothProfile.STATE_CONNECTED;
}
/**
* Handles headset client connection state change event
* @param intent must be {@link BluetoothHeadsetClient#ACTION_CONNECTION_STATE_CHANGED} intent
*/
@VisibleForTesting
void onHeadsetClientConnectionStateChanged(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
Log.e(TAG, "onHeadsetClientConnectionStateChanged() remote device is null");
return;
}
if (intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED)
== BluetoothProfile.STATE_DISCONNECTED
&& !hasBatteryService(device)) {
resetBatteryLevel(device, /*isBas=*/ false);
}
}
@VisibleForTesting
void onAgIndicatorValueChanged(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
Log.e(TAG, "onAgIndicatorValueChanged() remote device is null");
return;
}
if (intent.hasExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL)) {
int batteryLevel = intent.getIntExtra(BluetoothHeadsetClient.EXTRA_BATTERY_LEVEL, -1);
updateBatteryLevel(
device, batteryChargeIndicatorToPercentge(batteryLevel), /*isBas=*/ false);
}
}
private static void errorLog(String msg) {
Log.e(TAG, msg);
}
private static void debugLog(String msg) {
if (DBG) {
Log.d(TAG, msg);
}
}
private static void infoLog(String msg) {
if (DBG) {
Log.i(TAG, msg);
}
}
private static void warnLog(String msg) {
Log.w(TAG, msg);
}
}