blob: 68dc7a30ac072fc077e37f53158ca36f0b906173 [file] [log] [blame]
/*
* Copyright 2019 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.storage;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothA2dp.OptionalCodecsPreferenceStatus;
import android.bluetooth.BluetoothA2dp.OptionalCodecsSupportStatus;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProtoEnums;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
import android.util.Log;
import com.android.bluetooth.BluetoothStatsLog;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.EvictingQueue;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* The active device manager is responsible to handle a Room database
* for Bluetooth persistent data.
*/
public class DatabaseManager {
private static final String TAG = "BluetoothDatabase";
private AdapterService mAdapterService = null;
private HandlerThread mHandlerThread = null;
private Handler mHandler = null;
private MetadataDatabase mDatabase = null;
private boolean mMigratedFromSettingsGlobal = false;
@VisibleForTesting
final Map<String, Metadata> mMetadataCache = new HashMap<>();
private final Semaphore mSemaphore = new Semaphore(1);
private static final int METADATA_CHANGED_LOG_MAX_SIZE = 20;
private final EvictingQueue<String> mMetadataChangedLog;
private static final int LOAD_DATABASE_TIMEOUT = 500; // milliseconds
private static final int MSG_LOAD_DATABASE = 0;
private static final int MSG_UPDATE_DATABASE = 1;
private static final int MSG_DELETE_DATABASE = 2;
private static final int MSG_CLEAR_DATABASE = 100;
private static final String LOCAL_STORAGE = "LocalStorage";
private static final String
LEGACY_BTSNOOP_DEFAULT_MODE = "bluetooth_btsnoop_default_mode";
private static final String
LEGACY_HEADSET_PRIORITY_PREFIX = "bluetooth_headset_priority_";
private static final String
LEGACY_A2DP_SINK_PRIORITY_PREFIX = "bluetooth_a2dp_sink_priority_";
private static final String
LEGACY_A2DP_SRC_PRIORITY_PREFIX = "bluetooth_a2dp_src_priority_";
private static final String LEGACY_A2DP_SUPPORTS_OPTIONAL_CODECS_PREFIX =
"bluetooth_a2dp_supports_optional_codecs_";
private static final String LEGACY_A2DP_OPTIONAL_CODECS_ENABLED_PREFIX =
"bluetooth_a2dp_optional_codecs_enabled_";
private static final String
LEGACY_INPUT_DEVICE_PRIORITY_PREFIX = "bluetooth_input_device_priority_";
private static final String
LEGACY_MAP_PRIORITY_PREFIX = "bluetooth_map_priority_";
private static final String
LEGACY_MAP_CLIENT_PRIORITY_PREFIX = "bluetooth_map_client_priority_";
private static final String
LEGACY_PBAP_CLIENT_PRIORITY_PREFIX = "bluetooth_pbap_client_priority_";
private static final String
LEGACY_SAP_PRIORITY_PREFIX = "bluetooth_sap_priority_";
private static final String
LEGACY_PAN_PRIORITY_PREFIX = "bluetooth_pan_priority_";
private static final String
LEGACY_HEARING_AID_PRIORITY_PREFIX = "bluetooth_hearing_aid_priority_";
/**
* Constructor of the DatabaseManager
*/
public DatabaseManager(AdapterService service) {
mAdapterService = service;
mMetadataChangedLog = EvictingQueue.create(METADATA_CHANGED_LOG_MAX_SIZE);
}
class DatabaseHandler extends Handler {
DatabaseHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOAD_DATABASE: {
synchronized (mDatabase) {
List<Metadata> list;
try {
list = mDatabase.load();
} catch (IllegalStateException e) {
Log.e(TAG, "Unable to open database: " + e);
mDatabase = MetadataDatabase
.createDatabaseWithoutMigration(mAdapterService);
list = mDatabase.load();
}
compactLastConnectionTime(list);
cacheMetadata(list);
}
break;
}
case MSG_UPDATE_DATABASE: {
Metadata data = (Metadata) msg.obj;
synchronized (mDatabase) {
mDatabase.insert(data);
}
break;
}
case MSG_DELETE_DATABASE: {
String address = (String) msg.obj;
synchronized (mDatabase) {
mDatabase.delete(address);
}
break;
}
case MSG_CLEAR_DATABASE: {
synchronized (mDatabase) {
mDatabase.deleteAll();
}
break;
}
}
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) {
Log.e(TAG, "Received intent with null action");
return;
}
switch (action) {
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
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");
bondStateChanged(device, state);
break;
}
case BluetoothAdapter.ACTION_STATE_CHANGED: {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.STATE_OFF);
if (!mMigratedFromSettingsGlobal
&& state == BluetoothAdapter.STATE_TURNING_ON) {
migrateSettingsGlobal();
}
break;
}
}
}
};
void bondStateChanged(BluetoothDevice device, int state) {
synchronized (mMetadataCache) {
String address = device.getAddress();
if (state != BluetoothDevice.BOND_NONE) {
if (mMetadataCache.containsKey(address)) {
return;
}
createMetadata(address, false);
} else {
Metadata metadata = mMetadataCache.get(address);
if (metadata != null) {
mMetadataCache.remove(address);
deleteDatabase(metadata);
}
}
}
}
boolean isValidMetaKey(int key) {
switch (key) {
case BluetoothDevice.METADATA_MANUFACTURER_NAME:
case BluetoothDevice.METADATA_MODEL_NAME:
case BluetoothDevice.METADATA_SOFTWARE_VERSION:
case BluetoothDevice.METADATA_HARDWARE_VERSION:
case BluetoothDevice.METADATA_COMPANION_APP:
case BluetoothDevice.METADATA_MAIN_ICON:
case BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET:
case BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON:
case BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON:
case BluetoothDevice.METADATA_UNTETHERED_CASE_ICON:
case BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY:
case BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY:
case BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY:
case BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING:
case BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING:
case BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING:
case BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI:
return true;
}
Log.w(TAG, "Invalid metadata key " + key);
return false;
}
/**
* Set customized metadata to database with requested key
*/
@VisibleForTesting
public boolean setCustomMeta(BluetoothDevice device, int key, byte[] newValue) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "setCustomMeta: device is null");
return false;
}
if (!isValidMetaKey(key)) {
Log.e(TAG, "setCustomMeta: meta key invalid " + key);
return false;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
createMetadata(address, false);
}
Metadata data = mMetadataCache.get(address);
byte[] oldValue = data.getCustomizedMeta(key);
if (oldValue != null && Arrays.equals(oldValue, newValue)) {
Log.v(TAG, "setCustomMeta: metadata not changed.");
return true;
}
logManufacturerInfo(device, key, newValue);
logMetadataChange(address, "setCustomMeta key=" + key);
data.setCustomizedMeta(key, newValue);
updateDatabase(data);
mAdapterService.metadataChanged(address, key, newValue);
return true;
}
}
/**
* Get customized metadata from database with requested key
*/
@VisibleForTesting
public byte[] getCustomMeta(BluetoothDevice device, int key) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "getCustomMeta: device is null");
return null;
}
if (!isValidMetaKey(key)) {
Log.e(TAG, "getCustomMeta: meta key invalid " + key);
return null;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
Log.d(TAG, "getCustomMeta: device " + address + " is not in cache");
return null;
}
Metadata data = mMetadataCache.get(address);
return data.getCustomizedMeta(key);
}
}
/**
* Set the device profile connection policy
*
* @param device {@link BluetoothDevice} wish to set
* @param profile The Bluetooth profile; one of {@link BluetoothProfile#HEADSET},
* {@link BluetoothProfile#HEADSET_CLIENT}, {@link BluetoothProfile#A2DP},
* {@link BluetoothProfile#A2DP_SINK}, {@link BluetoothProfile#HID_HOST},
* {@link BluetoothProfile#PAN}, {@link BluetoothProfile#PBAP},
* {@link BluetoothProfile#PBAP_CLIENT}, {@link BluetoothProfile#MAP},
* {@link BluetoothProfile#MAP_CLIENT}, {@link BluetoothProfile#SAP},
* {@link BluetoothProfile#HEARING_AID}
* @param newConnectionPolicy the connectionPolicy to set; one of
* {@link BluetoothProfile.CONNECTION_POLICY_UNKNOWN},
* {@link BluetoothProfile.CONNECTION_POLICY_FORBIDDEN},
* {@link BluetoothProfile.CONNECTION_POLICY_ALLOWED}
*/
@VisibleForTesting
public boolean setProfileConnectionPolicy(BluetoothDevice device, int profile,
int newConnectionPolicy) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "setProfileConnectionPolicy: device is null");
return false;
}
if (newConnectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN
&& newConnectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
&& newConnectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
Log.e(TAG, "setProfileConnectionPolicy: invalid connection policy "
+ newConnectionPolicy);
return false;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
if (newConnectionPolicy == BluetoothProfile.CONNECTION_POLICY_UNKNOWN) {
return true;
}
createMetadata(address, false);
}
Metadata data = mMetadataCache.get(address);
int oldConnectionPolicy = data.getProfileConnectionPolicy(profile);
if (oldConnectionPolicy == newConnectionPolicy) {
Log.v(TAG, "setProfileConnectionPolicy connection policy not changed.");
return true;
}
String profileStr = BluetoothProfile.getProfileName(profile);
logMetadataChange(address, profileStr + " connection policy changed: "
+ ": " + oldConnectionPolicy + " -> " + newConnectionPolicy);
data.setProfileConnectionPolicy(profile, newConnectionPolicy);
updateDatabase(data);
return true;
}
}
/**
* Get the device profile connection policy
*
* @param device {@link BluetoothDevice} wish to get
* @param profile The Bluetooth profile; one of {@link BluetoothProfile#HEADSET},
* {@link BluetoothProfile#HEADSET_CLIENT}, {@link BluetoothProfile#A2DP},
* {@link BluetoothProfile#A2DP_SINK}, {@link BluetoothProfile#HID_HOST},
* {@link BluetoothProfile#PAN}, {@link BluetoothProfile#PBAP},
* {@link BluetoothProfile#PBAP_CLIENT}, {@link BluetoothProfile#MAP},
* {@link BluetoothProfile#MAP_CLIENT}, {@link BluetoothProfile#SAP},
* {@link BluetoothProfile#HEARING_AID}
* @return the profile connection policy of the device; one of
* {@link BluetoothProfile.CONNECTION_POLICY_UNKNOWN},
* {@link BluetoothProfile.CONNECTION_POLICY_FORBIDDEN},
* {@link BluetoothProfile.CONNECTION_POLICY_ALLOWED}
*/
@VisibleForTesting
public int getProfileConnectionPolicy(BluetoothDevice device, int profile) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "getProfileConnectionPolicy: device is null");
return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
Log.d(TAG, "getProfileConnectionPolicy: device " + address + " is not in cache");
return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
}
Metadata data = mMetadataCache.get(address);
int connectionPolicy = data.getProfileConnectionPolicy(profile);
Log.v(TAG, "getProfileConnectionPolicy: " + address + ", profile=" + profile
+ ", connectionPolicy = " + connectionPolicy);
return connectionPolicy;
}
}
/**
* Set the A2DP optional coedc support value
*
* @param device {@link BluetoothDevice} wish to set
* @param newValue the new A2DP optional coedc support value, one of
* {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORT_UNKNOWN},
* {@link BluetoothA2dp#OPTIONAL_CODECS_NOT_SUPPORTED},
* {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORTED}
*/
@VisibleForTesting
public void setA2dpSupportsOptionalCodecs(BluetoothDevice device, int newValue) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "setA2dpOptionalCodec: device is null");
return;
}
if (newValue != BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN
&& newValue != BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED
&& newValue != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
Log.e(TAG, "setA2dpSupportsOptionalCodecs: invalid value " + newValue);
return;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
return;
}
Metadata data = mMetadataCache.get(address);
int oldValue = data.a2dpSupportsOptionalCodecs;
if (oldValue == newValue) {
return;
}
logMetadataChange(address, "Supports optional codec changed: "
+ oldValue + " -> " + newValue);
data.a2dpSupportsOptionalCodecs = newValue;
updateDatabase(data);
}
}
/**
* Get the A2DP optional coedc support value
*
* @param device {@link BluetoothDevice} wish to get
* @return the A2DP optional coedc support value, one of
* {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORT_UNKNOWN},
* {@link BluetoothA2dp#OPTIONAL_CODECS_NOT_SUPPORTED},
* {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORTED},
*/
@VisibleForTesting
@OptionalCodecsSupportStatus
public int getA2dpSupportsOptionalCodecs(BluetoothDevice device) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "setA2dpOptionalCodec: device is null");
return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
Log.d(TAG, "getA2dpOptionalCodec: device " + address + " is not in cache");
return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN;
}
Metadata data = mMetadataCache.get(address);
return data.a2dpSupportsOptionalCodecs;
}
}
/**
* Set the A2DP optional coedc enabled value
*
* @param device {@link BluetoothDevice} wish to set
* @param newValue the new A2DP optional coedc enabled value, one of
* {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_UNKNOWN},
* {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_DISABLED},
* {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_ENABLED}
*/
@VisibleForTesting
public void setA2dpOptionalCodecsEnabled(BluetoothDevice device, int newValue) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "setA2dpOptionalCodecEnabled: device is null");
return;
}
if (newValue != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN
&& newValue != BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED
&& newValue != BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) {
Log.e(TAG, "setA2dpOptionalCodecsEnabled: invalid value " + newValue);
return;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
return;
}
Metadata data = mMetadataCache.get(address);
int oldValue = data.a2dpOptionalCodecsEnabled;
if (oldValue == newValue) {
return;
}
logMetadataChange(address, "Enable optional codec changed: "
+ oldValue + " -> " + newValue);
data.a2dpOptionalCodecsEnabled = newValue;
updateDatabase(data);
}
}
/**
* Get the A2DP optional coedc enabled value
*
* @param device {@link BluetoothDevice} wish to get
* @return the A2DP optional coedc enabled value, one of
* {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_UNKNOWN},
* {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_DISABLED},
* {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_ENABLED}
*/
@VisibleForTesting
@OptionalCodecsPreferenceStatus
public int getA2dpOptionalCodecsEnabled(BluetoothDevice device) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "getA2dpOptionalCodecEnabled: device is null");
return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
Log.d(TAG, "getA2dpOptionalCodecEnabled: device " + address + " is not in cache");
return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
}
Metadata data = mMetadataCache.get(address);
return data.a2dpOptionalCodecsEnabled;
}
}
/**
* Updates the time this device was last connected
*
* @param device is the remote bluetooth device for which we are setting the connection time
*/
public void setConnection(BluetoothDevice device, boolean isA2dpDevice) {
synchronized (mMetadataCache) {
Log.d(TAG, "setConnection: device=" + device + " and isA2dpDevice=" + isA2dpDevice);
if (device == null) {
Log.e(TAG, "setConnection: device is null");
return;
}
if (isA2dpDevice) {
resetActiveA2dpDevice();
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
Log.d(TAG, "setConnection: Creating new metadata entry for device: " + device);
createMetadata(address, isA2dpDevice);
return;
}
// Updates last_active_time to the current counter value and increments the counter
Metadata metadata = mMetadataCache.get(address);
metadata.last_active_time = MetadataDatabase.sCurrentConnectionNumber++;
// Only update is_active_a2dp_device if an a2dp device is connected
if (isA2dpDevice) {
metadata.is_active_a2dp_device = true;
}
Log.d(TAG, "Updating last connected time for device: " + device + " to "
+ metadata.last_active_time);
updateDatabase(metadata);
}
}
/**
* Sets is_active_device to false if currently true for device
*
* @param device is the remote bluetooth device with which we have disconnected a2dp
*/
public void setDisconnection(BluetoothDevice device) {
synchronized (mMetadataCache) {
if (device == null) {
Log.e(TAG, "setDisconnection: device is null");
return;
}
String address = device.getAddress();
if (!mMetadataCache.containsKey(address)) {
return;
}
// Updates last connected time to either current time if connected or -1 if disconnected
Metadata metadata = mMetadataCache.get(address);
if (metadata.is_active_a2dp_device) {
metadata.is_active_a2dp_device = false;
Log.d(TAG, "setDisconnection: Updating is_active_device to false for device: "
+ device);
updateDatabase(metadata);
}
}
}
/**
* Remove a2dpActiveDevice from the current active device in the connection order table
*/
private void resetActiveA2dpDevice() {
synchronized (mMetadataCache) {
Log.d(TAG, "resetActiveA2dpDevice()");
for (Map.Entry<String, Metadata> entry : mMetadataCache.entrySet()) {
Metadata metadata = entry.getValue();
if (metadata.is_active_a2dp_device) {
Log.d(TAG, "resetActiveA2dpDevice");
metadata.is_active_a2dp_device = false;
updateDatabase(metadata);
}
}
}
}
/**
* Gets the most recently connected bluetooth devices in order with most recently connected
* first and least recently connected last
*
* @return a {@link List} of {@link BluetoothDevice} representing connected bluetooth devices
* in order of most recently connected
*/
public List<BluetoothDevice> getMostRecentlyConnectedDevices() {
List<BluetoothDevice> mostRecentlyConnectedDevices = new ArrayList<>();
synchronized (mMetadataCache) {
List<Metadata> sortedMetadata = new ArrayList<>(mMetadataCache.values());
sortedMetadata.sort((o1, o2) -> Long.compare(o2.last_active_time, o1.last_active_time));
for (Metadata metadata : sortedMetadata) {
try {
mostRecentlyConnectedDevices.add(BluetoothAdapter.getDefaultAdapter()
.getRemoteDevice(metadata.getAddress()));
} catch (IllegalArgumentException ex) {
Log.d(TAG, "getBondedDevicesOrdered: Invalid address for "
+ "device " + metadata.getAddress());
}
}
}
return mostRecentlyConnectedDevices;
}
/**
* Gets the last active a2dp device
*
* @return the most recently active a2dp device or null if the last a2dp device was null
*/
public BluetoothDevice getMostRecentlyConnectedA2dpDevice() {
synchronized (mMetadataCache) {
for (Map.Entry<String, Metadata> entry : mMetadataCache.entrySet()) {
Metadata metadata = entry.getValue();
if (metadata.is_active_a2dp_device) {
try {
return BluetoothAdapter.getDefaultAdapter().getRemoteDevice(
metadata.getAddress());
} catch (IllegalArgumentException ex) {
Log.d(TAG, "getMostRecentlyConnectedA2dpDevice: Invalid address for "
+ "device " + metadata.getAddress());
}
}
}
}
return null;
}
/**
*
* @param metadataList is the list of metadata
*/
private void compactLastConnectionTime(List<Metadata> metadataList) {
Log.d(TAG, "compactLastConnectionTime: Compacting metadata after load");
MetadataDatabase.sCurrentConnectionNumber = 0;
// Have to go in reverse order as list is ordered by descending last_active_time
for (int index = metadataList.size() - 1; index >= 0; index--) {
Metadata metadata = metadataList.get(index);
if (metadata.last_active_time != MetadataDatabase.sCurrentConnectionNumber) {
Log.d(TAG, "compactLastConnectionTime: Setting last_active_item for device: "
+ metadata.getAddress() + " from " + metadata.last_active_time + " to "
+ MetadataDatabase.sCurrentConnectionNumber);
metadata.last_active_time = MetadataDatabase.sCurrentConnectionNumber;
updateDatabase(metadata);
MetadataDatabase.sCurrentConnectionNumber++;
}
}
}
/**
* Get the {@link Looper} for the handler thread. This is used in testing and helper
* objects
*
* @return {@link Looper} for the handler thread
*/
@VisibleForTesting
public Looper getHandlerLooper() {
if (mHandlerThread == null) {
return null;
}
return mHandlerThread.getLooper();
}
/**
* Start and initialize the DatabaseManager
*
* @param database the Bluetooth storage {@link MetadataDatabase}
*/
public void start(MetadataDatabase database) {
Log.d(TAG, "start()");
if (mAdapterService == null) {
Log.e(TAG, "stat failed, mAdapterService is null.");
return;
}
if (database == null) {
Log.e(TAG, "stat failed, database is null.");
return;
}
mDatabase = database;
mHandlerThread = new HandlerThread("BluetoothDatabaseManager");
mHandlerThread.start();
mHandler = new DatabaseHandler(mHandlerThread.getLooper());
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
mAdapterService.registerReceiver(mReceiver, filter);
loadDatabase();
}
String getDatabaseAbsolutePath() {
//TODO backup database when Bluetooth turn off and FOTA?
return mAdapterService.getDatabasePath(MetadataDatabase.DATABASE_NAME)
.getAbsolutePath();
}
/**
* Clear all persistence data in database
*/
public void factoryReset() {
Log.w(TAG, "factoryReset");
Message message = mHandler.obtainMessage(MSG_CLEAR_DATABASE);
mHandler.sendMessage(message);
}
/**
* Close and de-init the DatabaseManager
*/
public void cleanup() {
removeUnusedMetadata();
mAdapterService.unregisterReceiver(mReceiver);
if (mHandlerThread != null) {
mHandlerThread.quit();
mHandlerThread = null;
}
mMetadataCache.clear();
}
void createMetadata(String address, boolean isActiveA2dpDevice) {
Metadata data = new Metadata(address);
data.is_active_a2dp_device = isActiveA2dpDevice;
mMetadataCache.put(address, data);
updateDatabase(data);
logMetadataChange(address, "Metadata created");
}
@VisibleForTesting
void removeUnusedMetadata() {
BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
synchronized (mMetadataCache) {
mMetadataCache.forEach((address, metadata) -> {
if (!address.equals(LOCAL_STORAGE)
&& !Arrays.asList(bondedDevices).stream().anyMatch(device ->
address.equals(device.getAddress()))) {
List<Integer> list = metadata.getChangedCustomizedMeta();
for (int key : list) {
mAdapterService.metadataChanged(address, key, null);
}
Log.i(TAG, "remove unpaired device from database " + address);
deleteDatabase(mMetadataCache.get(address));
}
});
}
}
void cacheMetadata(List<Metadata> list) {
synchronized (mMetadataCache) {
Log.i(TAG, "cacheMetadata");
// Unlock the main thread.
mSemaphore.release();
if (!isMigrated(list)) {
// Wait for data migrate from Settings Global
mMigratedFromSettingsGlobal = false;
return;
}
mMigratedFromSettingsGlobal = true;
for (Metadata data : list) {
String address = data.getAddress();
Log.v(TAG, "cacheMetadata: found device " + address);
mMetadataCache.put(address, data);
}
Log.i(TAG, "cacheMetadata: Database is ready");
}
}
boolean isMigrated(List<Metadata> list) {
for (Metadata data : list) {
String address = data.getAddress();
if (address.equals(LOCAL_STORAGE) && data.migrated) {
return true;
}
}
return false;
}
void migrateSettingsGlobal() {
Log.i(TAG, "migrateSettingGlobal");
BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
ContentResolver contentResolver = mAdapterService.getContentResolver();
for (BluetoothDevice device : bondedDevices) {
int a2dpConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyA2dpSinkPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int a2dpSinkConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyA2dpSrcPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int hearingaidConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyHearingAidPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int headsetConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyHeadsetPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int headsetClientConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyHeadsetPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int hidHostConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyHidHostPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int mapConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyMapPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int mapClientConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyMapClientPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int panConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyPanPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int pbapConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyPbapClientPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int pbapClientConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacyPbapClientPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int sapConnectionPolicy = Settings.Global.getInt(contentResolver,
getLegacySapPriorityKey(device.getAddress()),
BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
int a2dpSupportsOptionalCodec = Settings.Global.getInt(contentResolver,
getLegacyA2dpSupportsOptionalCodecsKey(device.getAddress()),
BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
int a2dpOptionalCodecEnabled = Settings.Global.getInt(contentResolver,
getLegacyA2dpOptionalCodecsEnabledKey(device.getAddress()),
BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
String address = device.getAddress();
Metadata data = new Metadata(address);
data.setProfileConnectionPolicy(BluetoothProfile.A2DP, a2dpConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.A2DP_SINK, a2dpSinkConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.HEADSET, headsetConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.HEADSET_CLIENT,
headsetClientConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.HID_HOST, hidHostConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.PAN, panConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.PBAP, pbapConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.PBAP_CLIENT,
pbapClientConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.MAP, mapConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.MAP_CLIENT, mapClientConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.SAP, sapConnectionPolicy);
data.setProfileConnectionPolicy(BluetoothProfile.HEARING_AID,
hearingaidConnectionPolicy);
data.a2dpSupportsOptionalCodecs = a2dpSupportsOptionalCodec;
data.a2dpOptionalCodecsEnabled = a2dpOptionalCodecEnabled;
mMetadataCache.put(address, data);
updateDatabase(data);
}
// Mark database migrated from Settings Global
Metadata localData = new Metadata(LOCAL_STORAGE);
localData.migrated = true;
mMetadataCache.put(LOCAL_STORAGE, localData);
updateDatabase(localData);
// Reload database after migration is completed
loadDatabase();
}
/**
* Get the key that retrieves a bluetooth headset's priority.
*/
private static String getLegacyHeadsetPriorityKey(String address) {
return LEGACY_HEADSET_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth a2dp sink's priority.
*/
private static String getLegacyA2dpSinkPriorityKey(String address) {
return LEGACY_A2DP_SINK_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth a2dp src's priority.
*/
private static String getLegacyA2dpSrcPriorityKey(String address) {
return LEGACY_A2DP_SRC_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth a2dp device's ability to support optional codecs.
*/
private static String getLegacyA2dpSupportsOptionalCodecsKey(String address) {
return LEGACY_A2DP_SUPPORTS_OPTIONAL_CODECS_PREFIX
+ address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves whether a bluetooth a2dp device should have optional codecs
* enabled.
*/
private static String getLegacyA2dpOptionalCodecsEnabledKey(String address) {
return LEGACY_A2DP_OPTIONAL_CODECS_ENABLED_PREFIX
+ address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth Input Device's priority.
*/
private static String getLegacyHidHostPriorityKey(String address) {
return LEGACY_INPUT_DEVICE_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth pan client priority.
*/
private static String getLegacyPanPriorityKey(String address) {
return LEGACY_PAN_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth hearing aid priority.
*/
private static String getLegacyHearingAidPriorityKey(String address) {
return LEGACY_HEARING_AID_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth map priority.
*/
private static String getLegacyMapPriorityKey(String address) {
return LEGACY_MAP_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth map client priority.
*/
private static String getLegacyMapClientPriorityKey(String address) {
return LEGACY_MAP_CLIENT_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth pbap client priority.
*/
private static String getLegacyPbapClientPriorityKey(String address) {
return LEGACY_PBAP_CLIENT_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
/**
* Get the key that retrieves a bluetooth sap priority.
*/
private static String getLegacySapPriorityKey(String address) {
return LEGACY_SAP_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
}
private void loadDatabase() {
Log.d(TAG, "Load Database");
Message message = mHandler.obtainMessage(MSG_LOAD_DATABASE);
mHandler.sendMessage(message);
try {
// Lock the thread until handler thread finish loading database.
mSemaphore.tryAcquire(LOAD_DATABASE_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.e(TAG, "loadDatabase: semaphore acquire failed");
}
}
private void updateDatabase(Metadata data) {
if (data.getAddress() == null) {
Log.e(TAG, "updateDatabase: address is null");
return;
}
Log.d(TAG, "updateDatabase " + data.getAddress());
Message message = mHandler.obtainMessage(MSG_UPDATE_DATABASE);
message.obj = data;
mHandler.sendMessage(message);
}
@VisibleForTesting
void deleteDatabase(Metadata data) {
String address = data.getAddress();
if (address == null) {
Log.e(TAG, "deleteDatabase: address is null");
return;
}
logMetadataChange(address, "Metadata deleted");
Message message = mHandler.obtainMessage(MSG_DELETE_DATABASE);
message.obj = data.getAddress();
mHandler.sendMessage(message);
}
private void logManufacturerInfo(BluetoothDevice device, int key, byte[] bytesValue) {
String callingApp = mAdapterService.getPackageManager().getNameForUid(
Binder.getCallingUid());
String manufacturerName = "";
String modelName = "";
String hardwareVersion = "";
String softwareVersion = "";
String value = Utils.byteArrayToUtf8String(bytesValue);
switch (key) {
case BluetoothDevice.METADATA_MANUFACTURER_NAME:
manufacturerName = value;
break;
case BluetoothDevice.METADATA_MODEL_NAME:
modelName = value;
break;
case BluetoothDevice.METADATA_HARDWARE_VERSION:
hardwareVersion = value;
break;
case BluetoothDevice.METADATA_SOFTWARE_VERSION:
softwareVersion = value;
break;
default:
// Do not log anything if metadata doesn't fall into above categories
return;
}
BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_DEVICE_INFO_REPORTED,
mAdapterService.obfuscateAddress(device),
BluetoothProtoEnums.DEVICE_INFO_EXTERNAL, callingApp, manufacturerName, modelName,
hardwareVersion, softwareVersion, mAdapterService.getMetricId(device));
}
private void logMetadataChange(String address, String log) {
String time = Utils.getLocalTimeString();
String uidPid = Utils.getUidPidString();
mMetadataChangedLog.add(time + " (" + uidPid + ") " + address + " " + log);
}
/**
* Dump database info to a PrintWriter
*
* @param writer the PrintWriter to write log
*/
public void dump(PrintWriter writer) {
writer.println("\nBluetoothDatabase:");
writer.println(" Metadata Changes:");
for (String log : mMetadataChangedLog) {
writer.println(" " + log);
}
writer.println("\nMetadata:");
for (HashMap.Entry<String, Metadata> entry : mMetadataCache.entrySet()) {
if (entry.getKey().equals(LOCAL_STORAGE)) {
// No need to dump local storage
continue;
}
writer.println(" " + entry.getValue());
}
}
}