fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -0,0 +1,389 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class A2dpProfile implements LocalBluetoothProfile {
private static final String TAG = "A2dpProfile";
private Context mContext;
private BluetoothA2dp mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final BluetoothAdapter mBluetoothAdapter;
static final ParcelUuid[] SINK_UUIDS = {
BluetoothUuid.A2DP_SINK,
BluetoothUuid.ADV_AUDIO_DIST,
};
static final String NAME = "A2DP";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class A2dpServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothA2dp) proxy;
// We just bound to the service, so refresh the UI for any connected A2DP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "A2dpProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(A2dpProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
mProfileManager.callServiceDisconnectedListeners();
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.A2DP;
}
A2dpProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(context, new A2dpServiceListener(),
BluetoothProfile.A2DP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
/**
* Get A2dp devices matching connection states{
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Get A2dp devices matching connection states{
* @code BluetoothProfile.STATE_DISCONNECTED,
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_AUDIO)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_AUDIO);
}
public BluetoothDevice getActiveDevice() {
if (mBluetoothAdapter == null) return null;
final List<BluetoothDevice> activeDevices = mBluetoothAdapter
.getActiveDevices(BluetoothProfile.A2DP);
return (activeDevices.size() > 0) ? activeDevices.get(0) : null;
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
boolean isA2dpPlaying() {
if (mService == null) return false;
List<BluetoothDevice> sinks = mService.getConnectedDevices();
for (BluetoothDevice device : sinks) {
if (mService.isA2dpPlaying(device)) {
return true;
}
}
return false;
}
public boolean supportsHighQualityAudio(BluetoothDevice device) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
if (bluetoothDevice == null) {
return false;
}
int support = mService.isOptionalCodecsSupported(bluetoothDevice);
return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED;
}
/**
* @return whether high quality audio is enabled or not
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
if (bluetoothDevice == null) {
return false;
}
int enabled = mService.isOptionalCodecsEnabled(bluetoothDevice);
if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) {
return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED;
} else if (getConnectionStatus(bluetoothDevice) != BluetoothProfile.STATE_CONNECTED
&& supportsHighQualityAudio(bluetoothDevice)) {
// Since we don't have a stored preference and the device isn't connected, just return
// true since the default behavior when the device gets connected in the future would be
// to have optional codecs enabled.
return true;
}
BluetoothCodecConfig codecConfig = null;
if (mService.getCodecStatus(bluetoothDevice) != null) {
codecConfig = mService.getCodecStatus(bluetoothDevice).getCodecConfig();
}
if (codecConfig != null) {
return !codecConfig.isMandatoryCodec();
} else {
return false;
}
}
public void setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
if (bluetoothDevice == null) {
return;
}
int prefValue = enabled
? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED
: BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED;
mService.setOptionalCodecsEnabled(bluetoothDevice, prefValue);
if (getConnectionStatus(bluetoothDevice) != BluetoothProfile.STATE_CONNECTED) {
return;
}
if (enabled) {
mService.enableOptionalCodecs(bluetoothDevice);
} else {
mService.disableOptionalCodecs(bluetoothDevice);
}
}
/**
* Gets the label associated with the codec of a Bluetooth device.
*
* @param device to get codec label from
* @return the label associated with the device codec
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
if (bluetoothDevice == null || !supportsHighQualityAudio(device)
|| getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
return mContext.getString(unknownCodecId);
}
// We want to get the highest priority codec, since that's the one that will be used with
// this device, and see if it is high-quality (ie non-mandatory).
List<BluetoothCodecConfig> selectable = null;
if (mService.getCodecStatus(device) != null) {
selectable = mService.getCodecStatus(device).getCodecsSelectableCapabilities();
// To get the highest priority, we sort in reverse.
Collections.sort(selectable,
(a, b) -> {
return b.getCodecPriority() - a.getCodecPriority();
});
}
final BluetoothCodecConfig codecConfig = (selectable == null || selectable.size() < 1)
? null : selectable.get(0);
final int codecType = (codecConfig == null || codecConfig.isMandatoryCodec())
? BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID : codecConfig.getCodecType();
int index = -1;
switch (codecType) {
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
index = 1;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
index = 2;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
index = 3;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
index = 4;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
index = 5;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LC3:
index = 6;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_OPUS:
index = 7;
break;
}
if (index < 0) {
return mContext.getString(unknownCodecId);
}
return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality,
mContext.getResources().getStringArray(R.array.bluetooth_a2dp_codec_titles)[index]);
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_a2dp;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_a2dp_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_a2dp_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up A2DP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2015 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
final class A2dpSinkProfile implements LocalBluetoothProfile {
private static final String TAG = "A2dpSinkProfile";
private BluetoothA2dpSink mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final ParcelUuid[] SRC_UUIDS = {
BluetoothUuid.A2DP_SOURCE,
BluetoothUuid.ADV_AUDIO_DIST,
};
static final String NAME = "A2DPSink";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 5;
// These callbacks run on the main thread.
private final class A2dpSinkServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothA2dpSink) proxy;
// We just bound to the service, so refresh the UI for any connected A2DP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "A2dpSinkProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(A2dpSinkProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.A2DP_SINK;
}
A2dpSinkProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new A2dpSinkServiceListener(),
BluetoothProfile.A2DP_SINK);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
boolean isAudioPlaying() {
if (mService == null) {
return false;
}
List<BluetoothDevice> srcs = mService.getConnectedDevices();
if (!srcs.isEmpty()) {
if (mService.isAudioPlaying(srcs.get(0))) {
return true;
}
}
return false;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
// we need to have same string in UI for even SINK Media Audio.
return R.string.bluetooth_profile_a2dp;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_a2dp_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_a2dp_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP_SINK,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up A2DP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 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.settingslib.bluetooth;
public final class BluetoothBroadcastUtils {
/**
* The fragment tag specified to FragmentManager for container activities to manage fragments.
*/
public static final String TAG_FRAGMENT_QR_CODE_SCANNER = "qr_code_scanner_fragment";
/**
* Action for launching qr code scanner activity.
*/
public static final String ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER =
"android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER";
/**
* Extra for {@link android.bluetooth.BluetoothDevice}.
*/
public static final String EXTRA_BLUETOOTH_DEVICE_SINK = "bluetooth_device_sink";
/**
* Extra for checking the {@link android.bluetooth.BluetoothLeBroadcastAssistant} should perform
* this operation for all coordinated set members throughout one session or not.
*/
public static final String EXTRA_BLUETOOTH_SINK_IS_GROUP = "bluetooth_sink_is_group";
/**
* Bluetooth scheme.
*/
public static final String SCHEME_BT_BROADCAST_METADATA = "BLUETOOTH:UUID:184F;";
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.STATE_CONNECTED;
import static android.bluetooth.BluetoothAdapter.STATE_CONNECTING;
import static android.bluetooth.BluetoothAdapter.STATE_DISCONNECTED;
import static android.bluetooth.BluetoothAdapter.STATE_DISCONNECTING;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON;
import android.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* BluetoothCallback provides a callback interface for the settings
* UI to receive events from {@link BluetoothEventManager}.
*/
public interface BluetoothCallback {
/**
* It will be called when the state of the local Bluetooth adapter has been changed.
* It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_STATE_CHANGED}.
* For example, Bluetooth has been turned on or off.
*
* @param bluetoothState the current Bluetooth state, the possible values are:
* {@link android.bluetooth.BluetoothAdapter#STATE_OFF},
* {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_ON},
* {@link android.bluetooth.BluetoothAdapter#STATE_ON},
* {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_OFF}.
*/
default void onBluetoothStateChanged(@AdapterState int bluetoothState) {}
/**
* It will be called when the local Bluetooth adapter has started
* or finished the remote device discovery process.
* It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_DISCOVERY_STARTED} and
* {@link android.bluetooth.BluetoothAdapter#ACTION_DISCOVERY_FINISHED}.
*
* @param started indicate the current process is started or finished.
*/
default void onScanningStateChanged(boolean started) {}
/**
* It will be called in following situations:
* 1. In scanning mode, when a new device has been found.
* 2. When a profile service is connected and existing connected devices has been found.
* This API only invoked once for each device and all devices will be cached in
* {@link CachedBluetoothDeviceManager}.
*
* @param cachedDevice the Bluetooth device.
*/
default void onDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {}
/**
* It will be called when requiring to remove a remote device from CachedBluetoothDevice list
*
* @param cachedDevice the Bluetooth device.
*/
default void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {}
/**
* It will be called when bond state of a remote device is changed.
* It is listening {@link android.bluetooth.BluetoothDevice#ACTION_BOND_STATE_CHANGED}
*
* @param cachedDevice the Bluetooth device.
* @param bondState the Bluetooth device bond state, the possible values are:
* {@link android.bluetooth.BluetoothDevice#BOND_NONE},
* {@link android.bluetooth.BluetoothDevice#BOND_BONDING},
* {@link android.bluetooth.BluetoothDevice#BOND_BONDED}.
*/
default void onDeviceBondStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, int bondState) {}
/**
* It will be called in following situations:
* 1. When the adapter is not connected to any profiles of any remote devices
* and it attempts a connection to a profile.
* 2. When the adapter disconnects from the last profile of the last device.
* It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_CONNECTION_STATE_CHANGED}
*
* @param cachedDevice the Bluetooth device.
* @param state the Bluetooth device connection state, the possible values are:
* {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED},
* {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTING},
* {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED},
* {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTING}.
*/
default void onConnectionStateChanged(
@Nullable CachedBluetoothDevice cachedDevice,
@ConnectionState int state) {}
/**
* It will be called when device been set as active for {@code bluetoothProfile}
* It is listening in following intent:
* {@link android.bluetooth.BluetoothA2dp#ACTION_ACTIVE_DEVICE_CHANGED}
* {@link android.bluetooth.BluetoothHeadset#ACTION_ACTIVE_DEVICE_CHANGED}
* {@link android.bluetooth.BluetoothHearingAid#ACTION_ACTIVE_DEVICE_CHANGED}
*
* @param activeDevice the active Bluetooth device.
* @param bluetoothProfile the profile of active Bluetooth device.
*/
default void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
/**
* It will be called in following situations:
* 1. When the call state on the device is changed.
* 2. When the audio connection state of the A2DP profile is changed.
* It is listening in following intent:
* {@link android.bluetooth.BluetoothHeadset#ACTION_AUDIO_STATE_CHANGED}
* {@link android.telephony.TelephonyManager#ACTION_PHONE_STATE_CHANGED}
*/
default void onAudioModeChanged() {}
/**
* It will be called when one of the bluetooth device profile connection state is changed.
*
* @param cachedDevice the active Bluetooth device.
* @param state the BluetoothProfile connection state, the possible values are:
* {@link android.bluetooth.BluetoothProfile#STATE_CONNECTED},
* {@link android.bluetooth.BluetoothProfile#STATE_CONNECTING},
* {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTED},
* {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTING}.
* @param bluetoothProfile the BluetoothProfile id.
*/
default void onProfileConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice,
@ConnectionState int state,
int bluetoothProfile) {
}
/**
* Called when ACL connection state is changed. It listens to
* {@link android.bluetooth.BluetoothDevice#ACTION_ACL_CONNECTED} and {@link
* android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECTED}
*
* @param cachedDevice Bluetooth device that changed
* @param state the Bluetooth device connection state, the possible values are:
* {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED},
* {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED}
*/
default void onAclConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, int state) {}
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "STATE_" }, value = {
STATE_DISCONNECTED,
STATE_CONNECTING,
STATE_CONNECTED,
STATE_DISCONNECTING,
})
@interface ConnectionState {}
@IntDef(prefix = { "STATE_" }, value = {
STATE_OFF,
STATE_TURNING_ON,
STATE_ON,
STATE_TURNING_OFF,
})
@Retention(RetentionPolicy.SOURCE)
@interface AdapterState {}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothUuid;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.internal.util.ArrayUtils;
/**
* BluetoothDeviceFilter contains a static method that returns a
* Filter object that returns whether or not the BluetoothDevice
* passed to it matches the specified filter type constant from
* {@link android.bluetooth.BluetoothDevicePicker}.
*/
public final class BluetoothDeviceFilter {
private static final String TAG = "BluetoothDeviceFilter";
/** The filter interface to external classes. */
public interface Filter {
boolean matches(BluetoothDevice device);
}
/** All filter singleton (referenced directly). */
public static final Filter ALL_FILTER = new AllFilter();
/** Bonded devices only filter (referenced directly). */
public static final Filter BONDED_DEVICE_FILTER = new BondedDeviceFilter();
/** Unbonded devices only filter (referenced directly). */
public static final Filter UNBONDED_DEVICE_FILTER = new UnbondedDeviceFilter();
/** Table of singleton filter objects. */
private static final Filter[] FILTERS = {
ALL_FILTER, // FILTER_TYPE_ALL
new AudioFilter(), // FILTER_TYPE_AUDIO
new TransferFilter(), // FILTER_TYPE_TRANSFER
new PanuFilter(), // FILTER_TYPE_PANU
new NapFilter() // FILTER_TYPE_NAP
};
/** Private constructor. */
private BluetoothDeviceFilter() {
}
/**
* Returns the singleton {@link Filter} object for the specified type,
* or {@link #ALL_FILTER} if the type value is out of range.
*
* @param filterType a constant from BluetoothDevicePicker
* @return a singleton object implementing the {@link Filter} interface.
*/
public static Filter getFilter(int filterType) {
if (filterType >= 0 && filterType < FILTERS.length) {
return FILTERS[filterType];
} else {
Log.w(TAG, "Invalid filter type " + filterType + " for device picker");
return ALL_FILTER;
}
}
/** Filter that matches all devices. */
private static final class AllFilter implements Filter {
public boolean matches(BluetoothDevice device) {
return true;
}
}
/** Filter that matches only bonded devices. */
private static final class BondedDeviceFilter implements Filter {
public boolean matches(BluetoothDevice device) {
return device.getBondState() == BluetoothDevice.BOND_BONDED;
}
}
/** Filter that matches only unbonded devices. */
private static final class UnbondedDeviceFilter implements Filter {
public boolean matches(BluetoothDevice device) {
return device.getBondState() != BluetoothDevice.BOND_BONDED;
}
}
/** Parent class of filters based on UUID and/or Bluetooth class. */
private abstract static class ClassUuidFilter implements Filter {
abstract boolean matches(ParcelUuid[] uuids, BluetoothClass btClass);
public boolean matches(BluetoothDevice device) {
return matches(device.getUuids(), device.getBluetoothClass());
}
}
/** Filter that matches devices that support AUDIO profiles. */
private static final class AudioFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS)) {
return true;
}
if (BluetoothUuid.containsAnyUuid(uuids, HeadsetProfile.UUIDS)) {
return true;
}
} else if (btClass != null) {
if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)
|| doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
return true;
}
}
return false;
}
}
/** Filter that matches devices that support Object Transfer. */
private static final class TransferFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (ArrayUtils.contains(uuids, BluetoothUuid.OBEX_OBJECT_PUSH)) {
return true;
}
}
return btClass != null
&& doesClassMatch(btClass, BluetoothClass.PROFILE_OPP);
}
}
/** Filter that matches devices that support PAN User (PANU) profile. */
private static final class PanuFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (ArrayUtils.contains(uuids, BluetoothUuid.PANU)) {
return true;
}
}
return btClass != null
&& doesClassMatch(btClass, BluetoothClass.PROFILE_PANU);
}
}
/** Filter that matches devices that support NAP profile. */
private static final class NapFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (ArrayUtils.contains(uuids, BluetoothUuid.NAP)) {
return true;
}
}
return btClass != null
&& doesClassMatch(btClass, BluetoothClass.PROFILE_NAP);
}
}
@SuppressLint("NewApi") // Hidden API made public
private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
return btClass.doesClassMatch(classId);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
/* Required to handle timeout notification when phone is suspended */
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class BluetoothDiscoverableTimeoutReceiver extends BroadcastReceiver {
private static final String TAG = "BluetoothDiscoverableTimeoutReceiver";
private static final String INTENT_DISCOVERABLE_TIMEOUT =
"android.bluetooth.intent.DISCOVERABLE_TIMEOUT";
public static void setDiscoverableAlarm(Context context, long alarmTime) {
Log.d(TAG, "setDiscoverableAlarm(): alarmTime = " + alarmTime);
Intent intent = new Intent(INTENT_DISCOVERABLE_TIMEOUT);
intent.setClass(context, BluetoothDiscoverableTimeoutReceiver.class);
PendingIntent pending = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager =
(AlarmManager) context.getSystemService (Context.ALARM_SERVICE);
if (pending != null) {
// Cancel any previous alarms that do the same thing.
alarmManager.cancel(pending);
Log.d(TAG, "setDiscoverableAlarm(): cancel prev alarm");
}
pending = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pending);
}
public static void cancelDiscoverableAlarm(Context context) {
Log.d(TAG, "cancelDiscoverableAlarm(): Enter");
Intent intent = new Intent(INTENT_DISCOVERABLE_TIMEOUT);
intent.setClass(context, BluetoothDiscoverableTimeoutReceiver.class);
PendingIntent pending = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
if (pending != null) {
// Cancel any previous alarms that do the same thing.
AlarmManager alarmManager =
(AlarmManager) context.getSystemService (Context.ALARM_SERVICE);
alarmManager.cancel(pending);
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null || !intent.getAction().equals(INTENT_DISCOVERABLE_TIMEOUT)) {
return;
}
LocalBluetoothAdapter localBluetoothAdapter = LocalBluetoothAdapter.getInstance();
if(localBluetoothAdapter != null &&
localBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
Log.d(TAG, "Disable discoverable...");
localBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
} else {
Log.e(TAG, "localBluetoothAdapter is NULL!!");
}
}
}

View File

@@ -0,0 +1,555 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import static com.android.settingslib.flags.Flags.enableCachedBluetoothDeviceDedup;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.R;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* BluetoothEventManager receives broadcasts and callbacks from the Bluetooth
* API and dispatches the event on the UI thread to the right class in the
* Settings.
*/
public class BluetoothEventManager {
private static final String TAG = "BluetoothEventManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final LocalBluetoothAdapter mLocalAdapter;
private final CachedBluetoothDeviceManager mDeviceManager;
private final IntentFilter mAdapterIntentFilter, mProfileIntentFilter;
private final Map<String, Handler> mHandlerMap;
private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver();
private final BroadcastReceiver mProfileBroadcastReceiver = new BluetoothBroadcastReceiver();
private final Collection<BluetoothCallback> mCallbacks = new CopyOnWriteArrayList<>();
private final android.os.Handler mReceiverHandler;
private final UserHandle mUserHandle;
private final Context mContext;
interface Handler {
void onReceive(Context context, Intent intent, BluetoothDevice device);
}
/**
* Creates BluetoothEventManager with the ability to pass in {@link UserHandle} that tells it to
* listen for bluetooth events for that particular userHandle.
*
* <p> If passing in userHandle that's different from the user running the process,
* {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission is required. If
* userHandle passed in is {@code null}, we register event receiver for the
* {@code context.getUser()} handle.
*/
BluetoothEventManager(LocalBluetoothAdapter adapter,
CachedBluetoothDeviceManager deviceManager, Context context,
android.os.Handler handler, @Nullable UserHandle userHandle) {
mLocalAdapter = adapter;
mDeviceManager = deviceManager;
mAdapterIntentFilter = new IntentFilter();
mProfileIntentFilter = new IntentFilter();
mHandlerMap = new HashMap<>();
mContext = context;
mUserHandle = userHandle;
mReceiverHandler = handler;
// Bluetooth on/off broadcasts
addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());
// Generic connected/not broadcast
addHandler(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED,
new ConnectionStateChangedHandler());
// Discovery broadcasts
addHandler(BluetoothAdapter.ACTION_DISCOVERY_STARTED,
new ScanningStateChangedHandler(true));
addHandler(BluetoothAdapter.ACTION_DISCOVERY_FINISHED,
new ScanningStateChangedHandler(false));
addHandler(BluetoothDevice.ACTION_FOUND, new DeviceFoundHandler());
addHandler(BluetoothDevice.ACTION_NAME_CHANGED, new NameChangedHandler());
addHandler(BluetoothDevice.ACTION_ALIAS_CHANGED, new NameChangedHandler());
// Pairing broadcasts
addHandler(BluetoothDevice.ACTION_BOND_STATE_CHANGED, new BondStateChangedHandler());
// Fine-grained state broadcasts
addHandler(BluetoothDevice.ACTION_CLASS_CHANGED, new ClassChangedHandler());
addHandler(BluetoothDevice.ACTION_UUID, new UuidChangedHandler());
addHandler(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED, new BatteryLevelChangedHandler());
// Active device broadcasts
addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
addHandler(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
// Headset state changed broadcasts
addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
new AudioModeChangedHandler());
addHandler(TelephonyManager.ACTION_PHONE_STATE_CHANGED,
new AudioModeChangedHandler());
// ACL connection changed broadcasts
addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());
registerAdapterIntentReceiver();
}
/** Register to start receiving callbacks for Bluetooth events. */
public void registerCallback(BluetoothCallback callback) {
mCallbacks.add(callback);
}
/** Unregister to stop receiving callbacks for Bluetooth events. */
public void unregisterCallback(BluetoothCallback callback) {
mCallbacks.remove(callback);
}
@VisibleForTesting
void registerProfileIntentReceiver() {
registerIntentReceiver(mProfileBroadcastReceiver, mProfileIntentFilter);
}
@VisibleForTesting
void registerAdapterIntentReceiver() {
registerIntentReceiver(mBroadcastReceiver, mAdapterIntentFilter);
}
/**
* Registers the provided receiver to receive the broadcasts that correspond to the
* passed intent filter, in the context of the provided handler.
*/
private void registerIntentReceiver(BroadcastReceiver receiver, IntentFilter filter) {
if (mUserHandle == null) {
// If userHandle has not been provided, simply call registerReceiver.
mContext.registerReceiver(receiver, filter, null, mReceiverHandler,
Context.RECEIVER_EXPORTED);
} else {
// userHandle was explicitly specified, so need to call multi-user aware API.
mContext.registerReceiverAsUser(receiver, mUserHandle, filter, null, mReceiverHandler,
Context.RECEIVER_EXPORTED);
}
}
@VisibleForTesting
void addProfileHandler(String action, Handler handler) {
mHandlerMap.put(action, handler);
mProfileIntentFilter.addAction(action);
}
boolean readPairedDevices() {
Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices();
if (bondedDevices == null) {
return false;
}
boolean deviceAdded = false;
for (BluetoothDevice device : bondedDevices) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
mDeviceManager.addDevice(device);
deviceAdded = true;
}
}
return deviceAdded;
}
void dispatchDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceAdded(cachedDevice);
}
}
void dispatchDeviceRemoved(@NonNull CachedBluetoothDevice cachedDevice) {
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceDeleted(cachedDevice);
}
}
void dispatchProfileConnectionStateChanged(@NonNull CachedBluetoothDevice device, int state,
int bluetoothProfile) {
for (BluetoothCallback callback : mCallbacks) {
callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
}
}
private void dispatchConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
for (BluetoothCallback callback : mCallbacks) {
callback.onConnectionStateChanged(cachedDevice, state);
}
}
private void dispatchAudioModeChanged() {
for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) {
cachedDevice.onAudioModeChanged();
}
for (BluetoothCallback callback : mCallbacks) {
callback.onAudioModeChanged();
}
}
@VisibleForTesting
void dispatchActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
CachedBluetoothDevice targetDevice = activeDevice;
for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) {
// should report isActive from main device or it will cause trouble to other callers.
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
CachedBluetoothDevice finalTargetDevice = targetDevice;
if (targetDevice != null
&& ((subDevice != null && subDevice.equals(targetDevice))
|| cachedDevice.getMemberDevice().stream().anyMatch(
memberDevice -> memberDevice.equals(finalTargetDevice)))) {
Log.d(TAG,
"The active device is the sub/member device "
+ targetDevice.getDevice().getAnonymizedAddress()
+ ". change targetDevice as main device "
+ cachedDevice.getDevice().getAnonymizedAddress());
targetDevice = cachedDevice;
}
boolean isActiveDevice = cachedDevice.equals(targetDevice);
cachedDevice.onActiveDeviceChanged(isActiveDevice, bluetoothProfile);
mDeviceManager.onActiveDeviceChanged(cachedDevice);
}
for (BluetoothCallback callback : mCallbacks) {
callback.onActiveDeviceChanged(targetDevice, bluetoothProfile);
}
}
private void dispatchAclStateChanged(@NonNull CachedBluetoothDevice activeDevice, int state) {
for (BluetoothCallback callback : mCallbacks) {
callback.onAclConnectionStateChanged(activeDevice, state);
}
}
@VisibleForTesting
void addHandler(String action, Handler handler) {
mHandlerMap.put(action, handler);
mAdapterIntentFilter.addAction(action);
}
private class BluetoothBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
BluetoothDevice device = intent
.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Handler handler = mHandlerMap.get(action);
if (handler != null) {
handler.onReceive(context, intent, device);
}
}
}
private class AdapterStateChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
// update local profiles and get paired devices
mLocalAdapter.setBluetoothStateInt(state);
// send callback to update UI and possibly start scanning
for (BluetoothCallback callback : mCallbacks) {
callback.onBluetoothStateChanged(state);
}
// Inform CachedDeviceManager that the adapter state has changed
mDeviceManager.onBluetoothStateChanged(state);
}
}
private class ScanningStateChangedHandler implements Handler {
private final boolean mStarted;
ScanningStateChangedHandler(boolean started) {
mStarted = started;
}
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
for (BluetoothCallback callback : mCallbacks) {
callback.onScanningStateChanged(mStarted);
}
mDeviceManager.onScanningStateChanged(mStarted);
}
}
private class DeviceFoundHandler implements Handler {
public void onReceive(Context context, Intent intent,
BluetoothDevice device) {
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
final boolean isCoordinatedSetMember =
intent.getBooleanExtra(BluetoothDevice.EXTRA_IS_COORDINATED_SET_MEMBER, false);
// TODO Pick up UUID. They should be available for 2.1 devices.
// Skip for now, there's a bluez problem and we are not getting uuids even for 2.1.
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
cachedDevice = mDeviceManager.addDevice(device);
Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice "
+ cachedDevice.getDevice().getAnonymizedAddress());
} else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
&& !cachedDevice.getDevice().isConnected()) {
// Dispatch device add callback to show bonded but
// not connected devices in discovery mode
dispatchDeviceAdded(cachedDevice);
}
cachedDevice.setRssi(rssi);
cachedDevice.setJustDiscovered(true);
cachedDevice.setIsCoordinatedSetMember(isCoordinatedSetMember);
}
}
private class ConnectionStateChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
BluetoothAdapter.ERROR);
dispatchConnectionStateChanged(cachedDevice, state);
}
}
private class NameChangedHandler implements Handler {
public void onReceive(Context context, Intent intent,
BluetoothDevice device) {
mDeviceManager.onDeviceNameUpdated(device);
}
}
private class BondStateChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
if (device == null) {
Log.e(TAG, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
return;
}
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
if (mDeviceManager.onBondStateChangedIfProcess(device, bondState)) {
Log.d(TAG, "Should not update UI for the set member");
return;
}
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
Log.w(TAG, "Got bonding state changed for " + device +
", but we have no record of that device.");
cachedDevice = mDeviceManager.addDevice(device);
}
if (enableCachedBluetoothDeviceDedup() && bondState == BluetoothDevice.BOND_BONDED) {
mDeviceManager.removeDuplicateInstanceForIdentityAddress(device);
}
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceBondStateChanged(cachedDevice, bondState);
}
cachedDevice.onBondingStateChanged(bondState);
if (bondState == BluetoothDevice.BOND_NONE) {
// Check if we need to remove other Coordinated set member devices / Hearing Aid
// devices
if (DEBUG) {
Log.d(TAG, "BondStateChangedHandler: cachedDevice.getGroupId() = "
+ cachedDevice.getGroupId() + ", cachedDevice.getHiSyncId()= "
+ cachedDevice.getHiSyncId());
}
if (cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
|| cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
Log.d(TAG, "BondStateChangedHandler: Start onDeviceUnpaired");
mDeviceManager.onDeviceUnpaired(cachedDevice);
}
int reason = intent.getIntExtra(BluetoothDevice.EXTRA_UNBOND_REASON,
BluetoothDevice.ERROR);
showUnbondMessage(context, cachedDevice.getName(), reason);
}
}
/**
* Called when we have reached the unbonded state.
*
* @param reason one of the error reasons from
* BluetoothDevice.UNBOND_REASON_*
*/
private void showUnbondMessage(Context context, String name, int reason) {
if (DEBUG) {
Log.d(TAG, "showUnbondMessage() name : " + name + ", reason : " + reason);
}
int errorMsg;
switch (reason) {
case BluetoothDevice.UNBOND_REASON_AUTH_FAILED:
errorMsg = R.string.bluetooth_pairing_pin_error_message;
break;
case BluetoothDevice.UNBOND_REASON_AUTH_REJECTED:
errorMsg = R.string.bluetooth_pairing_rejected_error_message;
break;
case BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN:
errorMsg = R.string.bluetooth_pairing_device_down_error_message;
break;
case BluetoothDevice.UNBOND_REASON_DISCOVERY_IN_PROGRESS:
case BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT:
case BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS:
case BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED:
errorMsg = R.string.bluetooth_pairing_error_message;
break;
default:
Log.w(TAG,
"showUnbondMessage: Not displaying any message for reason: " + reason);
return;
}
BluetoothUtils.showError(context, name, errorMsg);
}
}
private class ClassChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice != null) {
cachedDevice.refresh();
}
}
}
private class UuidChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice != null) {
cachedDevice.onUuidChanged();
}
}
}
private class BatteryLevelChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice != null) {
cachedDevice.refresh();
}
}
}
private class ActiveDeviceChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
String action = intent.getAction();
if (action == null) {
Log.w(TAG, "ActiveDeviceChangedHandler: action is null");
return;
}
@Nullable
CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
int bluetoothProfile = 0;
if (Objects.equals(action, BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.A2DP;
} else if (Objects.equals(action, BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.HEADSET;
} else if (Objects.equals(action, BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.HEARING_AID;
} else if (Objects.equals(action,
BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.LE_AUDIO;
} else {
Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
return;
}
dispatchActiveDeviceChanged(activeDevice, bluetoothProfile);
}
}
private class AclStateChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "AclStateChangedHandler: device is null");
return;
}
// Avoid to notify Settings UI for Hearing Aid sub device.
if (mDeviceManager.isSubDevice(device)) {
return;
}
final String action = intent.getAction();
if (action == null) {
Log.w(TAG, "AclStateChangedHandler: action is null");
return;
}
final CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
if (activeDevice == null) {
Log.w(TAG, "AclStateChangedHandler: activeDevice is null");
return;
}
final int state;
switch (action) {
case BluetoothDevice.ACTION_ACL_CONNECTED:
state = BluetoothAdapter.STATE_CONNECTED;
break;
case BluetoothDevice.ACTION_ACL_DISCONNECTED:
state = BluetoothAdapter.STATE_DISCONNECTED;
break;
default:
Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
return;
}
dispatchAclStateChanged(activeDevice, state);
}
}
private class AudioModeChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
final String action = intent.getAction();
if (action == null) {
Log.w(TAG, "AudioModeChangedHandler() action is null");
return;
}
dispatchAudioModeChanged();
}
}
}

View File

@@ -0,0 +1,387 @@
/*
* Copyright (C) 2023 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.settingslib.bluetooth
import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothLeAudioCodecConfigMetadata
import android.bluetooth.BluetoothLeAudioContentMetadata
import android.bluetooth.BluetoothLeBroadcastChannel
import android.bluetooth.BluetoothLeBroadcastMetadata
import android.bluetooth.BluetoothLeBroadcastSubgroup
import android.os.Build
import android.util.Base64
import android.util.Log
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA
object BluetoothLeBroadcastMetadataExt {
private const val TAG = "BtLeBroadcastMetadataExt"
// Data Elements for directing Broadcast Assistants
private const val KEY_BT_BROADCAST_NAME = "BN"
private const val KEY_BT_ADVERTISER_ADDRESS_TYPE = "AT"
private const val KEY_BT_ADVERTISER_ADDRESS = "AD"
private const val KEY_BT_BROADCAST_ID = "BI"
private const val KEY_BT_BROADCAST_CODE = "BC"
private const val KEY_BT_STREAM_METADATA = "MD"
private const val KEY_BT_STANDARD_QUALITY = "SQ"
private const val KEY_BT_HIGH_QUALITY = "HQ"
// Extended Bluetooth URI Data Elements
private const val KEY_BT_ADVERTISING_SID = "AS"
private const val KEY_BT_PA_INTERVAL = "PI"
private const val KEY_BT_NUM_SUBGROUPS = "NS"
// Subgroup data elements
private const val KEY_BTSG_BIS_SYNC = "BS"
private const val KEY_BTSG_NUM_BISES = "NB"
private const val KEY_BTSG_METADATA = "SM"
// Vendor specific data, not being used
private const val KEY_BTVSD_VENDOR_DATA = "VS"
private const val DELIMITER_KEY_VALUE = ":"
private const val DELIMITER_ELEMENT = ";"
private const val SUFFIX_QR_CODE = ";;"
// BT constants
private const val BIS_SYNC_MAX_CHANNEL = 32
private const val BIS_SYNC_NO_PREFERENCE = 0xFFFFFFFFu
private const val SUBGROUP_LC3_CODEC_ID = 0x6L
/**
* Converts [BluetoothLeBroadcastMetadata] to QR code string.
*
* QR code string will prefix with "BLUETOOTH:UUID:184F".
*/
fun BluetoothLeBroadcastMetadata.toQrCodeString(): String {
val entries = mutableListOf<Pair<String, String>>()
// Generate data elements for directing Broadcast Assistants
require(this.broadcastName != null) { "Broadcast name is mandatory for QR code" }
entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString(
this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)))
entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS_TYPE, this.sourceAddressType.toString()))
entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS, this.sourceDevice.address.replace(":", "")))
entries.add(Pair(KEY_BT_BROADCAST_ID, String.format("%X", this.broadcastId.toLong())))
if (this.broadcastCode != null) {
entries.add(Pair(KEY_BT_BROADCAST_CODE,
Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP)))
}
if (this.publicBroadcastMetadata != null &&
this.publicBroadcastMetadata?.rawMetadata?.size != 0) {
entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString(
this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
}
if ((this.audioConfigQuality and
BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD) != 0) {
entries.add(Pair(KEY_BT_STANDARD_QUALITY, "1"))
}
if ((this.audioConfigQuality and
BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_HIGH) != 0) {
entries.add(Pair(KEY_BT_HIGH_QUALITY, "1"))
}
// Generate extended Bluetooth URI data elements
entries.add(Pair(KEY_BT_ADVERTISING_SID,
String.format("%X", this.sourceAdvertisingSid.toLong())))
entries.add(Pair(KEY_BT_PA_INTERVAL, String.format("%X", this.paSyncInterval.toLong())))
entries.add(Pair(KEY_BT_NUM_SUBGROUPS, String.format("%X", this.subgroups.size.toLong())))
this.subgroups.forEach {
val (bisSync, bisCount) = getBisSyncFromChannels(it.channels)
entries.add(Pair(KEY_BTSG_BIS_SYNC, String.format("%X", bisSync.toLong())))
if (bisCount > 0u) {
entries.add(Pair(KEY_BTSG_NUM_BISES, String.format("%X", bisCount.toLong())))
}
if (it.contentMetadata.rawMetadata.size != 0) {
entries.add(Pair(KEY_BTSG_METADATA,
Base64.encodeToString(it.contentMetadata.rawMetadata, Base64.NO_WRAP)))
}
}
val qrCodeString = SCHEME_BT_BROADCAST_METADATA +
entries.toQrCodeString(DELIMITER_ELEMENT) + SUFFIX_QR_CODE
Log.d(TAG, "Generated QR string : $qrCodeString")
return qrCodeString
}
/**
* Converts QR code string to [BluetoothLeBroadcastMetadata].
*
* QR code string should prefix with "BLUETOOTH:UUID:184F".
*/
fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? {
if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) {
Log.e(TAG, "String \"$qrCodeString\" does not begin with " +
"\"$SCHEME_BT_BROADCAST_METADATA\"")
return null
}
return try {
Log.d(TAG, "Parsing QR string: $qrCodeString")
val strippedString =
qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
.removeSuffix(SUFFIX_QR_CODE)
Log.d(TAG, "Stripped to: $strippedString")
parseQrCodeToMetadata(strippedString)
} catch (e: Exception) {
Log.w(TAG, "Cannot parse: $qrCodeString", e)
null
}
}
private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String {
val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second }
return entryStrings.joinToString(separator = delimiter)
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
private fun parseQrCodeToMetadata(input: String): BluetoothLeBroadcastMetadata {
// Split into a list of list
val elementFields = input.split(DELIMITER_ELEMENT)
.map{it.split(DELIMITER_KEY_VALUE, limit = 2)}
var sourceAddrType = BluetoothDevice.ADDRESS_TYPE_UNKNOWN
var sourceAddrString: String? = null
var sourceAdvertiserSid = -1
var broadcastId = -1
var broadcastName: String? = null
var streamMetadata: BluetoothLeAudioContentMetadata? = null
var paSyncInterval = -1
var broadcastCode: ByteArray? = null
var audioConfigQualityStandard = -1
var audioConfigQualityHigh = -1
var numSubgroups = -1
// List of subgroup data
var subgroupBisSyncList = mutableListOf<UInt>()
var subgroupNumOfBisesList = mutableListOf<UInt>()
var subgroupMetadataList = mutableListOf<ByteArray?>()
val builder = BluetoothLeBroadcastMetadata.Builder()
for (field: List<String> in elementFields) {
if (field.isEmpty()) {
continue
}
val key = field[0]
// Ignore 3rd value and after
val value = if (field.size > 1) field[1] else ""
when (key) {
// Parse data elements for directing Broadcast Assistants
KEY_BT_BROADCAST_NAME -> {
require(broadcastName == null) { "Duplicate broadcastName: $input" }
broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
}
KEY_BT_ADVERTISER_ADDRESS_TYPE -> {
require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
"Duplicate sourceAddrType: $input"
}
sourceAddrType = value.toInt()
}
KEY_BT_ADVERTISER_ADDRESS -> {
require(sourceAddrString == null) { "Duplicate sourceAddr: $input" }
sourceAddrString = value.chunked(2).joinToString(":")
}
KEY_BT_BROADCAST_ID -> {
require(broadcastId == -1) { "Duplicate broadcastId: $input" }
broadcastId = value.toInt(16)
}
KEY_BT_BROADCAST_CODE -> {
require(broadcastCode == null) { "Duplicate broadcastCode: $input" }
broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) }
.toByteArray(), Base64.NO_WRAP)
}
KEY_BT_STREAM_METADATA -> {
require(streamMetadata == null) {
"Duplicate streamMetadata $input"
}
streamMetadata = BluetoothLeAudioContentMetadata
.fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
}
KEY_BT_STANDARD_QUALITY -> {
require(audioConfigQualityStandard == -1) {
"Duplicate audioConfigQualityStandard: $input"
}
audioConfigQualityStandard = value.toInt()
}
KEY_BT_HIGH_QUALITY -> {
require(audioConfigQualityHigh == -1) {
"Duplicate audioConfigQualityHigh: $input"
}
audioConfigQualityHigh = value.toInt()
}
// Parse extended Bluetooth URI data elements
KEY_BT_ADVERTISING_SID -> {
require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
sourceAdvertiserSid = value.toInt(16)
}
KEY_BT_PA_INTERVAL -> {
require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" }
paSyncInterval = value.toInt(16)
}
KEY_BT_NUM_SUBGROUPS -> {
require(numSubgroups == -1) { "Duplicate numSubgroups: $input" }
numSubgroups = value.toInt(16)
}
// Repeatable subgroup elements
KEY_BTSG_BIS_SYNC -> {
subgroupBisSyncList.add(value.toUInt(16))
}
KEY_BTSG_NUM_BISES -> {
subgroupNumOfBisesList.add(value.toUInt(16))
}
KEY_BTSG_METADATA -> {
subgroupMetadataList.add(Base64.decode(value, Base64.NO_WRAP))
}
}
}
Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " +
"sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
"broadcastId=$broadcastId, broadcastName=$broadcastName, " +
"streamMetadata=${streamMetadata != null}, " +
"paSyncInterval=$paSyncInterval, " +
"broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " +
"audioConfigQualityStandard=$audioConfigQualityStandard, " +
"audioConfigQualityHigh=$audioConfigQualityHigh")
val adapter = BluetoothAdapter.getDefaultAdapter()
// Check parsed elements data
require(broadcastName != null) {
"broadcastName($broadcastName) must present in QR code string"
}
var addr = sourceAddrString
var addrType = sourceAddrType
if (sourceAddrString != null) {
require(sourceAddrType != BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
"sourceAddrType($sourceAddrType) must present if address present"
}
} else {
// Use placeholder device if not present
addr = "FF:FF:FF:FF:FF:FF"
addrType = BluetoothDevice.ADDRESS_TYPE_RANDOM
}
val device = adapter.getRemoteLeDevice(requireNotNull(addr), addrType)
// add source device and set broadcast code
var audioConfigQuality = BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE or
(if (audioConfigQualityStandard != -1) audioConfigQualityStandard else 0) or
(if (audioConfigQualityHigh != -1) audioConfigQualityHigh else 0)
// process subgroup data
// metadata should include at least 1 subgroup for metadata, add a placeholder group if not present
numSubgroups = if (numSubgroups > 0) numSubgroups else 1
for (i in 0 until numSubgroups) {
val bisSync = subgroupBisSyncList.getOrNull(i)
val bisNum = subgroupNumOfBisesList.getOrNull(i)
val metadata = subgroupMetadataList.getOrNull(i)
val channels = convertToChannels(bisSync, bisNum)
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
.setAudioLocation(0).build()
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
setCodecId(SUBGROUP_LC3_CODEC_ID)
setCodecSpecificConfig(audioCodecConfigMetadata)
setContentMetadata(
BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0)))
channels.forEach(::addChannel)
}.build()
Log.d(TAG, "parseQrCodeToMetadata: subgroup $i elements bisSync=$bisSync, " +
"bisNum=$bisNum, metadata=${metadata != null}")
builder.addSubgroup(subgroup)
}
builder.apply {
setSourceDevice(device, sourceAddrType)
setSourceAdvertisingSid(sourceAdvertiserSid)
setBroadcastId(broadcastId)
setBroadcastName(broadcastName)
// QR code should set PBP(public broadcast profile) for auracast
setPublicBroadcast(true)
setPublicBroadcastMetadata(streamMetadata)
setPaSyncInterval(paSyncInterval)
setEncrypted(broadcastCode != null)
setBroadcastCode(broadcastCode)
// Presentation delay is unknown and not useful when adding source
// Broadcast sink needs to sync to the Broadcast source to get presentation delay
setPresentationDelayMicros(0)
setAudioConfigQuality(audioConfigQuality)
}
return builder.build()
}
private fun getBisSyncFromChannels(
channels: List<BluetoothLeBroadcastChannel>
): Pair<UInt, UInt> {
var bisSync = 0u
var bisCount = 0u
// channel index starts from 1
channels.forEach { channel ->
if (channel.channelIndex > 0) {
bisCount++
if (channel.isSelected) {
bisSync = bisSync or (1u shl (channel.channelIndex - 1))
}
}
}
// No channel is selected means no preference on Android platform
return if (bisSync == 0u) Pair(BIS_SYNC_NO_PREFERENCE, bisCount)
else Pair(bisSync, bisCount)
}
private fun convertToChannels(
bisSync: UInt?,
bisNum: UInt?
): List<BluetoothLeBroadcastChannel> {
Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisNum=$bisNum")
// if no BIS_SYNC or BIS_NUM available or BIS_SYNC is no preference
// return empty channel map with one placeholder channel
var selectedChannels = if (bisSync != null && bisNum != null) bisSync else 0u
val channels = mutableListOf<BluetoothLeBroadcastChannel>()
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
.setAudioLocation(0).build()
if (bisSync == BIS_SYNC_NO_PREFERENCE || selectedChannels == 0u) {
// No channel preference means no channel is selected
// Generate one placeholder channel for metadata
val channel = BluetoothLeBroadcastChannel.Builder().apply {
setSelected(false)
setChannelIndex(1)
setCodecMetadata(audioCodecConfigMetadata)
}
return listOf(channel.build())
}
for (i in 0 until BIS_SYNC_MAX_CHANNEL) {
val channelMask = 1u shl i
if ((selectedChannels and channelMask) != 0u) {
val channel = BluetoothLeBroadcastChannel.Builder().apply {
setSelected(true)
setChannelIndex(i + 1)
setCodecMetadata(audioCodecConfigMetadata)
}
channels.add(channel.build())
}
}
return channels
}
}

View File

@@ -0,0 +1,719 @@
package com.android.settingslib.bluetooth;
import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.provider.DeviceConfig;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.graphics.drawable.IconCompat;
import com.android.settingslib.R;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BluetoothUtils {
private static final String TAG = "BluetoothUtils";
public static final boolean V = false; // verbose logging
public static final boolean D = true; // regular logging
public static final int META_INT_ERROR = -1;
public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
private static final Set<String> EXCLUSIVE_MANAGERS = ImmutableSet.of(
"com.google.android.gms.dck");
private static ErrorListener sErrorListener;
public static int getConnectionStateSummary(int connectionState) {
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_connected;
case BluetoothProfile.STATE_CONNECTING:
return R.string.bluetooth_connecting;
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_disconnected;
case BluetoothProfile.STATE_DISCONNECTING:
return R.string.bluetooth_disconnecting;
default:
return 0;
}
}
static void showError(Context context, String name, int messageResId) {
if (sErrorListener != null) {
sErrorListener.onShowError(context, name, messageResId);
}
}
public static void setErrorListener(ErrorListener listener) {
sErrorListener = listener;
}
public interface ErrorListener {
void onShowError(Context context, String name, int messageResId);
}
/**
* @param context to access resources from
* @param cachedDevice to get class from
* @return pair containing the drawable and the description of the Bluetooth class
* of the device.
*/
public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context,
CachedBluetoothDevice cachedDevice) {
BluetoothClass btClass = cachedDevice.getBtClass();
if (btClass != null) {
switch (btClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
return new Pair<>(getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_laptop),
context.getString(R.string.bluetooth_talkback_computer));
case BluetoothClass.Device.Major.PHONE:
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_phone),
context.getString(R.string.bluetooth_talkback_phone));
case BluetoothClass.Device.Major.PERIPHERAL:
return new Pair<>(
getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)),
context.getString(R.string.bluetooth_talkback_input_peripheral));
case BluetoothClass.Device.Major.IMAGING:
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_settings_print),
context.getString(R.string.bluetooth_talkback_imaging));
default:
// unrecognized device class; continue
}
}
if (cachedDevice.isHearingAidDevice()) {
return new Pair<>(getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_hearing_aid),
context.getString(R.string.bluetooth_talkback_hearing_aids));
}
List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
int resId = 0;
for (LocalBluetoothProfile profile : profiles) {
int profileResId = profile.getDrawableResource(btClass);
if (profileResId != 0) {
// The device should show hearing aid icon if it contains any hearing aid related
// profiles
if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
return new Pair<>(getBluetoothDrawable(context, profileResId),
context.getString(R.string.bluetooth_talkback_hearing_aids));
}
if (resId == 0) {
resId = profileResId;
}
}
}
if (resId != 0) {
return new Pair<>(getBluetoothDrawable(context, resId), null);
}
if (btClass != null) {
if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_headset_hfp),
context.getString(R.string.bluetooth_talkback_headset));
}
if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) {
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_headphones_a2dp),
context.getString(R.string.bluetooth_talkback_headphone));
}
}
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_settings_bluetooth).mutate(),
context.getString(R.string.bluetooth_talkback_bluetooth));
}
/**
* Get bluetooth drawable by {@code resId}
*/
public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) {
return context.getDrawable(resId);
}
/**
* Get colorful bluetooth icon with description
*/
public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(Context context,
CachedBluetoothDevice cachedDevice) {
final Resources resources = context.getResources();
final Pair<Drawable, String> pair = BluetoothUtils.getBtDrawableWithDescription(context,
cachedDevice);
if (pair.first instanceof BitmapDrawable) {
return new Pair<>(new AdaptiveOutlineDrawable(
resources, ((BitmapDrawable) pair.first).getBitmap()), pair.second);
}
int hashCode;
if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) {
hashCode = new Integer(cachedDevice.getGroupId()).hashCode();
} else {
hashCode = cachedDevice.getAddress().hashCode();
}
return new Pair<>(buildBtRainbowDrawable(context,
pair.first, hashCode), pair.second);
}
/**
* Build Bluetooth device icon with rainbow
*/
private static Drawable buildBtRainbowDrawable(Context context, Drawable drawable,
int hashCode) {
final Resources resources = context.getResources();
// Deal with normal headset
final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors);
final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors);
// get color index based on mac address
final int index = Math.abs(hashCode % iconBgColors.length);
drawable.setTint(iconFgColors[index]);
final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable);
((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]);
return adaptiveIcon;
}
/**
* Get bluetooth icon with description
*/
public static Pair<Drawable, String> getBtDrawableWithDescription(Context context,
CachedBluetoothDevice cachedDevice) {
final Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription(
context, cachedDevice);
final BluetoothDevice bluetoothDevice = cachedDevice.getDevice();
final int iconSize = context.getResources().getDimensionPixelSize(
R.dimen.bt_nearby_icon_size);
final Resources resources = context.getResources();
// Deal with advanced device icon
if (isAdvancedDetailsHeader(bluetoothDevice)) {
final Uri iconUri = getUriMetaData(bluetoothDevice,
BluetoothDevice.METADATA_MAIN_ICON);
if (iconUri != null) {
try {
context.getContentResolver().takePersistableUriPermission(iconUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException e) {
Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e);
}
try {
final Bitmap bitmap = MediaStore.Images.Media.getBitmap(
context.getContentResolver(), iconUri);
if (bitmap != null) {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
iconSize, false);
bitmap.recycle();
return new Pair<>(new BitmapDrawable(resources,
resizedBitmap), pair.second);
}
} catch (IOException e) {
Log.e(TAG, "Failed to get drawable for: " + iconUri, e);
} catch (SecurityException e) {
Log.e(TAG, "Failed to get permission for: " + iconUri, e);
}
}
}
return new Pair<>(pair.first, pair.second);
}
/**
* Check if the Bluetooth device supports advanced metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @return true if it supports advanced metadata, false otherwise.
*/
public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) {
if (!isAdvancedHeaderEnabled()) {
return false;
}
if (isUntetheredHeadset(bluetoothDevice)) {
return true;
}
// The metadata is for Android S
String deviceType = getStringMetaData(bluetoothDevice,
BluetoothDevice.METADATA_DEVICE_TYPE);
if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)
|| TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH)
|| TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT)
|| TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) {
Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType);
return true;
}
return false;
}
/**
* Check if the Bluetooth device is supports advanced metadata and an untethered headset
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @return true if it supports advanced metadata and an untethered headset, false otherwise.
*/
public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) {
if (!isAdvancedHeaderEnabled()) {
return false;
}
if (isUntetheredHeadset(bluetoothDevice)) {
return true;
}
// The metadata is for Android S
String deviceType = getStringMetaData(bluetoothDevice,
BluetoothDevice.METADATA_DEVICE_TYPE);
if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) {
Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device ");
return true;
}
return false;
}
/**
* Check if a device class matches with a defined BluetoothClass device.
*
* @param device Must be one of the public constants in {@link BluetoothClass.Device}
* @return true if device class matches, false otherwise.
*/
public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice,
int device) {
final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass();
return bluetoothClass != null && bluetoothClass.getDeviceClass() == device;
}
private static boolean isAdvancedHeaderEnabled() {
if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED,
true)) {
Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
return false;
}
return true;
}
private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) {
// The metadata is for Android R
if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true");
return true;
}
return false;
}
/**
* Create an Icon pointing to a drawable.
*/
public static IconCompat createIconWithDrawable(Drawable drawable) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
final int width = drawable.getIntrinsicWidth();
final int height = drawable.getIntrinsicHeight();
bitmap = createBitmap(drawable,
width > 0 ? width : 1,
height > 0 ? height : 1);
}
return IconCompat.createWithBitmap(bitmap);
}
/**
* Build device icon with advanced outline
*/
public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
final int iconSize = context.getResources().getDimensionPixelSize(
R.dimen.advanced_icon_size);
final Resources resources = context.getResources();
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
final int width = drawable.getIntrinsicWidth();
final int height = drawable.getIntrinsicHeight();
bitmap = createBitmap(drawable,
width > 0 ? width : 1,
height > 0 ? height : 1);
}
if (bitmap != null) {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
iconSize, false);
bitmap.recycle();
return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED);
}
return drawable;
}
/**
* Creates a drawable with specified width and height.
*/
public static Bitmap createBitmap(Drawable drawable, int width, int height) {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
/**
* Get boolean Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the boolean metdata
*/
public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return false;
}
final byte[] data = bluetoothDevice.getMetadata(key);
if (data == null) {
return false;
}
return Boolean.parseBoolean(new String(data));
}
/**
* Get String Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the String metdata
*/
public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return null;
}
final byte[] data = bluetoothDevice.getMetadata(key);
if (data == null) {
return null;
}
return new String(data);
}
/**
* Get integer Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the int metdata
*/
public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return META_INT_ERROR;
}
final byte[] data = bluetoothDevice.getMetadata(key);
if (data == null) {
return META_INT_ERROR;
}
try {
return Integer.parseInt(new String(data));
} catch (NumberFormatException e) {
return META_INT_ERROR;
}
}
/**
* Get URI Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the URI metdata
*/
public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) {
String data = getStringMetaData(bluetoothDevice, key);
if (data == null) {
return null;
}
return Uri.parse(data);
}
/**
* Get URI Bluetooth metadata for extra control
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @return the URI metadata
*/
public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) {
String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data);
}
/**
* Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means:
* 1) currently connected
* 2) is Hearing Aid or LE Audio
* OR
* 3) connected profile matches currentAudioProfile
*
* @param cachedDevice the CachedBluetoothDevice
* @param audioManager audio manager to get the current audio profile
* @return if the device is AvailableMediaBluetoothDevice
*/
@WorkerThread
public static boolean isAvailableMediaBluetoothDevice(
CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
int audioMode = audioManager.getMode();
int currentAudioProfile;
if (audioMode == AudioManager.MODE_RINGTONE
|| audioMode == AudioManager.MODE_IN_CALL
|| audioMode == AudioManager.MODE_IN_COMMUNICATION) {
// in phone call
currentAudioProfile = BluetoothProfile.HEADSET;
} else {
// without phone call
currentAudioProfile = BluetoothProfile.A2DP;
}
boolean isFilterMatched = false;
if (isDeviceConnected(cachedDevice)) {
// If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
// It would show in Available Devices group.
if (cachedDevice.isConnectedAshaHearingAidDevice()
|| cachedDevice.isConnectedLeAudioDevice()) {
Log.d(TAG, "isFilterMatched() device : "
+ cachedDevice.getName() + ", the profile is connected.");
return true;
}
// According to the current audio profile type,
// this page will show the bluetooth device that have corresponding profile.
// For example:
// If current audio profile is a2dp, show the bluetooth device that have a2dp profile.
// If current audio profile is headset,
// show the bluetooth device that have headset profile.
switch (currentAudioProfile) {
case BluetoothProfile.A2DP:
isFilterMatched = cachedDevice.isConnectedA2dpDevice();
break;
case BluetoothProfile.HEADSET:
isFilterMatched = cachedDevice.isConnectedHfpDevice();
break;
}
}
return isFilterMatched;
}
/**
* Check if the Bluetooth device is a ConnectedBluetoothDevice, which means:
* 1) currently connected
* 2) is not Hearing Aid or LE Audio
* AND
* 3) connected profile does not match currentAudioProfile
*
* @param cachedDevice the CachedBluetoothDevice
* @param audioManager audio manager to get the current audio profile
* @return if the device is AvailableMediaBluetoothDevice
*/
@WorkerThread
public static boolean isConnectedBluetoothDevice(
CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
int audioMode = audioManager.getMode();
int currentAudioProfile;
if (audioMode == AudioManager.MODE_RINGTONE
|| audioMode == AudioManager.MODE_IN_CALL
|| audioMode == AudioManager.MODE_IN_COMMUNICATION) {
// in phone call
currentAudioProfile = BluetoothProfile.HEADSET;
} else {
// without phone call
currentAudioProfile = BluetoothProfile.A2DP;
}
boolean isFilterMatched = false;
if (isDeviceConnected(cachedDevice)) {
// If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
// It would not show in Connected Devices group.
if (cachedDevice.isConnectedAshaHearingAidDevice()
|| cachedDevice.isConnectedLeAudioDevice()) {
return false;
}
// According to the current audio profile type,
// this page will show the bluetooth device that doesn't have corresponding profile.
// For example:
// If current audio profile is a2dp,
// show the bluetooth device that doesn't have a2dp profile.
// If current audio profile is headset,
// show the bluetooth device that doesn't have headset profile.
switch (currentAudioProfile) {
case BluetoothProfile.A2DP:
isFilterMatched = !cachedDevice.isConnectedA2dpDevice();
break;
case BluetoothProfile.HEADSET:
isFilterMatched = !cachedDevice.isConnectedHfpDevice();
break;
}
}
return isFilterMatched;
}
/**
* Check if the Bluetooth device is an active media device
*
* @param cachedDevice the CachedBluetoothDevice
* @return if the Bluetooth device is an active media device
*/
public static boolean isActiveMediaDevice(CachedBluetoothDevice cachedDevice) {
return cachedDevice.isActiveDevice(BluetoothProfile.A2DP)
|| cachedDevice.isActiveDevice(BluetoothProfile.HEADSET)
|| cachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)
|| cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
}
/**
* Check if the Bluetooth device is an active LE Audio device
*
* @param cachedDevice the CachedBluetoothDevice
* @return if the Bluetooth device is an active LE Audio device
*/
public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
}
private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) {
return false;
}
final BluetoothDevice device = cachedDevice.getDevice();
return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
}
@SuppressLint("NewApi") // Hidden API made public
private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
return btClass.doesClassMatch(classId);
}
private static String extraTagValue(String tag, String metaData) {
if (TextUtils.isEmpty(metaData)) {
return null;
}
Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)"));
Matcher matcher = pattern.matcher(metaData);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private static String getTagStart(String tag) {
return String.format(Locale.ENGLISH, "<%s>", tag);
}
private static String getTagEnd(String tag) {
return String.format(Locale.ENGLISH, "</%s>", tag);
}
private static String generateExpressionWithTag(String tag, String value) {
return getTagStart(tag) + value + getTagEnd(tag);
}
/**
* Returns the BluetoothDevice's exclusive manager
* ({@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the
* given set, otherwise null.
*/
@Nullable
private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) {
byte[] exclusiveManagerNameBytes = bluetoothDevice.getMetadata(
BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
if (exclusiveManagerNameBytes == null) {
Log.d(TAG, "Bluetooth device " + bluetoothDevice.getName()
+ " doesn't have exclusive manager");
return null;
}
String exclusiveManagerName = new String(exclusiveManagerNameBytes);
return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName
: null;
}
/**
* Checks if given package is installed
*/
private static boolean isPackageInstalled(Context context,
String packageName) {
PackageManager packageManager = context.getPackageManager();
try {
packageManager.getPackageInfo(packageName, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Package " + packageName + " is not installed");
}
return false;
}
/**
* A BluetoothDevice is exclusively managed if
* 1) it has field {@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata.
* 2) the exclusive manager app name is in the allowlist.
* 3) the exclusive manager app is installed.
*/
public static boolean isExclusivelyManagedBluetoothDevice(@NonNull Context context,
@NonNull BluetoothDevice bluetoothDevice) {
String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice);
if (exclusiveManagerName == null) {
return false;
}
if (!isPackageInstalled(context, exclusiveManagerName)) {
return false;
} else {
Log.d(TAG, "Found exclusively managed app " + exclusiveManagerName);
return true;
}
}
/**
* Return the allowlist for exclusive manager names.
*/
@NonNull
public static Set<String> getExclusiveManagers() {
return EXCLUSIVE_MANAGERS;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (C) 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.settingslib.bluetooth;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import com.android.settingslib.R;
public class BroadcastDialog extends AlertDialog {
private static final String TAG = "BroadcastDialog";
private String mCurrentApp;
private String mSwitchApp;
private Context mContext;
public BroadcastDialog(Context context) {
super(context);
mContext = context;
}
@Override
public void onCreate(Bundle savedInstanceState) {
View layout = View.inflate(mContext, R.layout.broadcast_dialog, null);
final Window window = getWindow();
window.setContentView(layout);
window.setWindowAnimations(
com.android.settingslib.widget.theme.R.style.Theme_AlertDialog_SettingsLib);
TextView title = layout.findViewById(R.id.dialog_title);
TextView subTitle = layout.findViewById(R.id.dialog_subtitle);
title.setText(mContext.getString(R.string.bt_le_audio_broadcast_dialog_title, mCurrentApp));
subTitle.setText(
mContext.getString(R.string.bt_le_audio_broadcast_dialog_sub_title, mSwitchApp));
Button positiveBtn = layout.findViewById(R.id.positive_btn);
Button negativeBtn = layout.findViewById(R.id.negative_btn);
Button neutralBtn = layout.findViewById(R.id.neutral_btn);
positiveBtn.setText(mContext.getString(
R.string.bt_le_audio_broadcast_dialog_switch_app, mSwitchApp), null);
neutralBtn.setOnClickListener((view) -> {
Log.d(TAG, "BroadcastDialog dismiss.");
dismiss();
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,570 @@
/*
* Copyright (C) 2008 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.settingslib.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
*/
public class CachedBluetoothDeviceManager {
private static final String TAG = "CachedBluetoothDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
@VisibleForTesting static int sLateBondingTimeoutMillis = 5000; // 5s
private Context mContext;
private final LocalBluetoothManager mBtManager;
@VisibleForTesting
final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>();
@VisibleForTesting
HearingAidDeviceManager mHearingAidDeviceManager;
@VisibleForTesting
CsipDeviceManager mCsipDeviceManager;
BluetoothDevice mOngoingSetMemberPair;
boolean mIsLateBonding;
int mGroupIdOfLateBonding;
public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
mContext = context;
mBtManager = localBtManager;
mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager,
mCachedDevices);
mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices);
}
public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
return new ArrayList<>(mCachedDevices);
}
public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) {
cachedDevice.setJustDiscovered(false);
return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE;
}
public void onDeviceNameUpdated(BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null) {
cachedDevice.refreshName();
}
}
/**
* Search for existing {@link CachedBluetoothDevice} or return null
* if this device isn't in the cache. Use {@link #addDevice}
* to create and return a new {@link CachedBluetoothDevice} for
* a newly discovered {@link BluetoothDevice}.
*
* @param device the address of the Bluetooth device
* @return the cached device object for this device, or null if it has
* not been previously seen
*/
public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (cachedDevice.getDevice().equals(device)) {
return cachedDevice;
}
// Check the member devices for the coordinated set if it exists
final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.getDevice().equals(device)) {
return memberDevice;
}
}
}
// Check sub devices for hearing aid if it exists
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.getDevice().equals(device)) {
return subDevice;
}
}
return null;
}
/**
* Create and return a new {@link CachedBluetoothDevice}. This assumes
* that {@link #findDevice} has already been called and returned null.
* @param device the new Bluetooth device
* @return the newly created CachedBluetoothDevice object
*/
public CachedBluetoothDevice addDevice(BluetoothDevice device) {
return addDevice(device, /*leScanFilters=*/null);
}
/**
* Create and return a new {@link CachedBluetoothDevice}. This assumes
* that {@link #findDevice} has already been called and returned null.
* @param device the new Bluetooth device
* @param leScanFilters the BLE scan filters which the device matched
* @return the newly created CachedBluetoothDevice object
*/
public CachedBluetoothDevice addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters) {
CachedBluetoothDevice newDevice;
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
synchronized (this) {
newDevice = findDevice(device);
if (newDevice == null) {
newDevice = new CachedBluetoothDevice(mContext, profileManager, device);
mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice);
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice, leScanFilters);
if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice)
&& !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
mCachedDevices.add(newDevice);
mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
}
}
}
return newDevice;
}
/**
* Returns device summary of the pair of the hearing aid / CSIP passed as the parameter.
*
* @param CachedBluetoothDevice device
* @return Device summary, or if the pair does not exist or if it is not a hearing aid or
* a CSIP set member, then {@code null}.
*/
public synchronized String getSubDeviceSummary(CachedBluetoothDevice device) {
final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
// TODO: check the CSIP group size instead of the real member device set size, and adjust
// the size restriction.
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.isConnected()) {
return memberDevice.getConnectionSummary();
}
}
}
CachedBluetoothDevice subDevice = device.getSubDevice();
if (subDevice != null && subDevice.isConnected()) {
return subDevice.getConnectionSummary();
}
return null;
}
/**
* Search for existing sub device {@link CachedBluetoothDevice}.
*
* @param device the address of the Bluetooth device
* @return true for found sub / member device or false.
*/
public synchronized boolean isSubDevice(BluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (!cachedDevice.getDevice().equals(device)) {
// Check the member devices of the coordinated set if it exists
Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.getDevice().equals(device)) {
return true;
}
}
continue;
}
// Check sub devices of hearing aid if it exists
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.getDevice().equals(device)) {
return true;
}
}
}
return false;
}
/**
* Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
* Hearing Aid Service is connected and the HiSyncId's are now available.
*/
public synchronized void updateHearingAidsDevices() {
mHearingAidDeviceManager.updateHearingAidsDevices();
}
/**
* Updates the Csip devices; specifically the GroupId's. This routine is called when the
* CSIS is connected and the GroupId's are now available.
*/
public synchronized void updateCsipDevices() {
mCsipDeviceManager.updateCsipDevices();
}
/**
* Attempts to get the name of a remote device, otherwise returns the address.
*
* @param device The remote device.
* @return The name, or if unavailable, the address.
*/
public String getName(BluetoothDevice device) {
if (isOngoingPairByCsip(device)) {
CachedBluetoothDevice firstDevice =
mCsipDeviceManager.getFirstMemberDevice(mGroupIdOfLateBonding);
if (firstDevice != null && firstDevice.getName() != null) {
return firstDevice.getName();
}
}
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null && cachedDevice.getName() != null) {
return cachedDevice.getName();
}
String name = device.getAlias();
if (name != null) {
return name;
}
return device.getAddress();
}
public synchronized void clearNonBondedDevices() {
clearNonBondedSubDevices();
final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>();
mCachedDevices.stream()
.filter(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE)
.forEach(cachedDevice -> {
cachedDevice.release();
removedCachedDevice.add(cachedDevice);
});
mCachedDevices.removeAll(removedCachedDevice);
}
private void clearNonBondedSubDevices() {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (Object it : memberDevices.toArray()) {
CachedBluetoothDevice memberDevice = (CachedBluetoothDevice) it;
// Member device exists and it is not bonded
if (memberDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
cachedDevice.removeMemberDevice(memberDevice);
}
}
return;
}
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null
&& subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
// Sub device exists and it is not bonded
subDevice.release();
cachedDevice.setSubDevice(null);
}
}
}
public synchronized void onScanningStateChanged(boolean started) {
if (!started) return;
// If starting a new scan, clear old visibility
// Iterate in reverse order since devices may be removed.
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
cachedDevice.setJustDiscovered(false);
final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
memberDevice.setJustDiscovered(false);
}
return;
}
final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null) {
subDevice.setJustDiscovered(false);
}
}
}
public synchronized void onBluetoothStateChanged(int bluetoothState) {
// When Bluetooth is turning off, we need to clear the non-bonded devices
// Otherwise, they end up showing up on the next BT enable
if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.removeMemberDevice(memberDevice);
}
}
} else {
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null) {
if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.setSubDevice(null);
}
}
}
if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.setJustDiscovered(false);
cachedDevice.release();
mCachedDevices.remove(i);
}
}
// To clear the SetMemberPair flag when the Bluetooth is turning off.
mOngoingSetMemberPair = null;
mIsLateBonding = false;
mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}
synchronized void removeDuplicateInstanceForIdentityAddress(BluetoothDevice device) {
String identityAddress = device.getIdentityAddress();
if (identityAddress == null || identityAddress.equals(device.getAddress())) {
return;
}
mCachedDevices.removeIf(d -> {
boolean shouldRemove = d.getDevice().getAddress().equals(identityAddress);
if (shouldRemove) {
Log.d(TAG, "Remove instance for identity address " + d);
}
return shouldRemove;
});
}
public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice
cachedDevice, int state, int profileId) {
if (profileId == BluetoothProfile.HEARING_AID) {
return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
state);
}
if (profileId == BluetoothProfile.HEADSET
|| profileId == BluetoothProfile.A2DP
|| profileId == BluetoothProfile.LE_AUDIO
|| profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
state);
}
return false;
}
/** Handles when the device been set as active/inactive. */
public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) {
if (cachedBluetoothDevice.isHearingAidDevice()) {
mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice);
}
}
public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device);
// Should iterate through the cloned set to avoid ConcurrentModificationException
final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(device.getMemberDevice());
if (!memberDevices.isEmpty()) {
// Main device is unpaired, also unpair the member devices
for (CachedBluetoothDevice memberDevice : memberDevices) {
memberDevice.unpair();
memberDevice.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
device.removeMemberDevice(memberDevice);
}
} else if (mainDevice != null) {
// Member device is unpaired, also unpair the main device
mainDevice.unpair();
}
mainDevice = mHearingAidDeviceManager.findMainDevice(device);
CachedBluetoothDevice subDevice = device.getSubDevice();
if (subDevice != null) {
// Main device is unpaired, to unpair sub device
subDevice.unpair();
device.setSubDevice(null);
} else if (mainDevice != null) {
// Sub device unpaired, to unpair main device
mainDevice.unpair();
mainDevice.setSubDevice(null);
}
}
/**
* Called when we found a set member of a group. The function will check the {@code groupId} if
* it exists and the bond state of the device is BOND_NOE, and if there isn't any ongoing pair
* , and then return {@code true} to pair the device automatically.
*
* @param device The found device
* @param groupId The group id of the found device
*
* @return {@code true}, if the device should pair automatically; Otherwise, return
* {@code false}.
*/
private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null;
int bondState = device.getBondState();
boolean groupExists = mCsipDeviceManager.isExistedGroupId(groupId);
Log.d(TAG,
"isOngoingSetMemberPair=" + isOngoingSetMemberPair + ", bondState=" + bondState
+ ", groupExists=" + groupExists + ", groupId=" + groupId);
if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE || !groupExists) {
return false;
}
return true;
}
private synchronized boolean checkLateBonding(int groupId) {
CachedBluetoothDevice firstDevice = mCsipDeviceManager.getFirstMemberDevice(groupId);
if (firstDevice == null) {
Log.d(TAG, "No first device in group: " + groupId);
return false;
}
Timestamp then = firstDevice.getBondTimestamp();
if (then == null) {
Log.d(TAG, "No bond timestamp");
return true;
}
Timestamp now = new Timestamp(System.currentTimeMillis());
long diff = (now.getTime() - then.getTime());
Log.d(TAG, "Time difference to first bonding: " + diff + "ms");
return diff > sLateBondingTimeoutMillis;
}
/**
* Called to check if there is an ongoing bonding for the device and it is late bonding.
* If the device is not matching the ongoing bonding device then false will be returned.
*
* @param device The device to check.
*/
public synchronized boolean isLateBonding(BluetoothDevice device) {
if (!isOngoingPairByCsip(device)) {
Log.d(TAG, "isLateBonding: pair not ongoing or not matching device");
return false;
}
Log.d(TAG, "isLateBonding: " + mIsLateBonding);
return mIsLateBonding;
}
/**
* Called when we found a set member of a group. The function will check the {@code groupId} if
* it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair
* , and then pair the device automatically.
*
* @param device The found device
* @param groupId The group id of the found device
*/
public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) {
if (!shouldPairByCsip(device, groupId)) {
return;
}
Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " groupId=" + groupId + " by CSIP ");
mOngoingSetMemberPair = device;
mIsLateBonding = checkLateBonding(groupId);
mGroupIdOfLateBonding = groupId;
syncConfigFromMainDevice(device, groupId);
if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) {
Log.d(TAG, "Bonding could not be started");
mOngoingSetMemberPair = null;
mIsLateBonding = false;
mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}
private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) {
if (!isOngoingPairByCsip(device)) {
return;
}
CachedBluetoothDevice memberDevice = findDevice(device);
CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice);
if (mainDevice == null) {
mainDevice = mCsipDeviceManager.getCachedDevice(groupId);
}
if (mainDevice == null || mainDevice.equals(memberDevice)) {
Log.d(TAG, "no mainDevice");
return;
}
// The memberDevice set PhonebookAccessPermission
device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission());
}
/**
* Called when the bond state change. If the bond state change is related with the
* ongoing set member pair, the cachedBluetoothDevice will be created but the UI
* would not be updated. For the other case, return {@code false} to go through the normal
* flow.
*
* @param device The device
* @param bondState The new bond state
*
* @return {@code true}, if the bond state change for the device is handled inside this
* function, and would not like to update the UI. If not, return {@code false}.
*/
public synchronized boolean onBondStateChangedIfProcess(BluetoothDevice device, int bondState) {
if (!isOngoingPairByCsip(device)) {
return false;
}
if (bondState == BluetoothDevice.BOND_BONDING) {
return true;
}
mOngoingSetMemberPair = null;
mIsLateBonding = false;
mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
if (bondState != BluetoothDevice.BOND_NONE) {
if (findDevice(device) == null) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
CachedBluetoothDevice newDevice =
new CachedBluetoothDevice(mContext, profileManager, device);
mCachedDevices.add(newDevice);
findDevice(device).connect();
}
}
return true;
}
/**
* Check if the device is the one which is initial paired locally by CSIP. The setting
* would depned on it to accept the pairing request automatically
*
* @param device The device
*
* @return {@code true}, if the device is ongoing pair by CSIP. Otherwise, return
* {@code false}.
*/
public boolean isOngoingPairByCsip(BluetoothDevice device) {
return mOngoingSetMemberPair != null && mOngoingSetMemberPair.equals(device);
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}

View File

@@ -0,0 +1,401 @@
/*
* Copyright (C) 2021 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.settingslib.bluetooth;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
*/
public class CsipDeviceManager {
private static final String TAG = "CsipDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
private final LocalBluetoothManager mBtManager;
private final List<CachedBluetoothDevice> mCachedDevices;
CsipDeviceManager(LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> cachedDevices) {
mBtManager = localBtManager;
mCachedDevices = cachedDevices;
}
void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) {
// Current it only supports the base uuid for CSIP and group this set in UI.
final int groupId = getBaseGroupId(newDevice.getDevice());
if (isValidGroupId(groupId)) {
log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")");
// Once groupId is valid, assign groupId
newDevice.setGroupId(groupId);
}
}
private int getBaseGroupId(BluetoothDevice device) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final CsipSetCoordinatorProfile profileProxy = profileManager
.getCsipSetCoordinatorProfile();
if (profileProxy != null) {
final Map<Integer, ParcelUuid> groupIdMap = profileProxy
.getGroupUuidMapByDevice(device);
if (groupIdMap == null) {
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
for (Map.Entry<Integer, ParcelUuid> entry : groupIdMap.entrySet()) {
if (entry.getValue().equals(BluetoothUuid.CAP)) {
return entry.getKey();
}
}
}
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) {
final int groupId = newDevice.getGroupId();
if (isValidGroupId(groupId)) {
final CachedBluetoothDevice mainDevice = getCachedDevice(groupId);
log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice);
// Just add one of the coordinated set from a pair in the list that is shown in the UI.
// Once there is other devices with the same groupId, to add new device as member
// devices.
if (mainDevice != null) {
mainDevice.addMemberDevice(newDevice);
newDevice.setName(mainDevice.getName());
return true;
}
}
return false;
}
private boolean isValidGroupId(int groupId) {
return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
/**
* To find the device with {@code groupId}.
*
* @param groupId The group id
* @return if we could find a device with this {@code groupId} return this device. Otherwise,
* return null.
*/
public CachedBluetoothDevice getCachedDevice(int groupId) {
log("getCachedDevice: groupId: " + groupId);
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getGroupId() == groupId) {
log("getCachedDevice: found cachedDevice with the groupId: "
+ cachedDevice.getDevice().getAnonymizedAddress());
return cachedDevice;
}
}
return null;
}
// To collect all set member devices and call #onGroupIdChanged to group device by GroupId
void updateCsipDevices() {
final Set<Integer> newGroupIdSet = new HashSet<Integer>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
// Do nothing if GroupId has been assigned
if (!isValidGroupId(cachedDevice.getGroupId())) {
final int newGroupId = getBaseGroupId(cachedDevice.getDevice());
// Do nothing if there is no GroupId on Bluetooth device
if (isValidGroupId(newGroupId)) {
cachedDevice.setGroupId(newGroupId);
newGroupIdSet.add(newGroupId);
}
}
}
for (int groupId : newGroupIdSet) {
onGroupIdChanged(groupId);
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
private static boolean isAtLeastT() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
}
// Group devices by groupId
@VisibleForTesting
void onGroupIdChanged(int groupId) {
if (!isValidGroupId(groupId)) {
log("onGroupIdChanged: groupId is invalid");
return;
}
updateRelationshipOfGroupDevices(groupId);
}
// @return {@code true}, the event is processed inside the method. It is for updating
// le audio device on group relationship when receiving connected or disconnected.
// @return {@code false}, it is not le audio device or to process it same as other profiles
boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
int state) {
log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state);
if (state != BluetoothProfile.STATE_CONNECTED
&& state != BluetoothProfile.STATE_DISCONNECTED) {
return false;
}
return updateRelationshipOfGroupDevices(cachedDevice.getGroupId());
}
@VisibleForTesting
boolean updateRelationshipOfGroupDevices(int groupId) {
if (!isValidGroupId(groupId)) {
log("The device is not group.");
return false;
}
log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString());
// Get the preferred main device by getPreferredMainDeviceWithoutConectionState
List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId);
CachedBluetoothDevice preferredMainDevice =
getPreferredMainDevice(groupId, groupDevicesList);
log("The preferredMainDevice= " + preferredMainDevice
+ " and the groupDevicesList of groupId= " + groupId
+ " =" + groupDevicesList);
return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice);
}
CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
if (device == null || mCachedDevices == null) {
return null;
}
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (isValidGroupId(cachedDevice.getGroupId())) {
Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
if (memberSet.isEmpty()) {
continue;
}
for (CachedBluetoothDevice memberDevice : memberSet) {
if (memberDevice != null && memberDevice.equals(device)) {
return cachedDevice;
}
}
}
}
return null;
}
/**
* Check if the {@code groupId} is existed.
*
* @param groupId The group id
* @return {@code true}, if we could find a device with this {@code groupId}; Otherwise,
* return {@code false}.
*/
public boolean isExistedGroupId(int groupId) {
return getCachedDevice(groupId) != null;
}
@VisibleForTesting
List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) {
List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>();
if (!isValidGroupId(groupId)) {
return groupDevicesList;
}
for (CachedBluetoothDevice item : mCachedDevices) {
if (groupId != item.getGroupId()) {
continue;
}
groupDevicesList.add(item);
groupDevicesList.addAll(item.getMemberDevice());
}
return groupDevicesList;
}
public CachedBluetoothDevice getFirstMemberDevice(int groupId) {
List<CachedBluetoothDevice> members = getGroupDevicesFromAllOfDevicesList(groupId);
if (members.isEmpty())
return null;
CachedBluetoothDevice firstMember = members.get(0);
log("getFirstMemberDevice: groupId=" + groupId
+ " address=" + firstMember.getDevice().getAnonymizedAddress());
return firstMember;
}
@VisibleForTesting
CachedBluetoothDevice getPreferredMainDevice(int groupId,
List<CachedBluetoothDevice> groupDevicesList) {
// How to select the preferred main device?
// 1. The DUAL mode connected device which has A2DP/HFP and LE audio.
// 2. One of connected LE device in the list. Default is the lead device from LE profile.
// 3. If there is no connected device, then reset the relationship. Set the DUAL mode
// deviced as the main device. Otherwise, set any one of the device.
if (groupDevicesList == null || groupDevicesList.isEmpty()) {
return null;
}
CachedBluetoothDevice dualModeDevice = groupDevicesList.stream()
.filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
.anyMatch(profile -> profile instanceof LeAudioProfile))
.filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
.anyMatch(profile -> profile instanceof A2dpProfile
|| profile instanceof HeadsetProfile))
.findFirst().orElse(null);
if (isDeviceConnected(dualModeDevice)) {
log("getPreferredMainDevice: The connected DUAL mode device");
return dualModeDevice;
}
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT())
? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
if (leAudioLeadDevice != null) {
log("getPreferredMainDevice: The LeadDevice from LE profile is "
+ leAudioLeadDevice.getAnonymizedAddress());
}
CachedBluetoothDevice leAudioLeadCachedDevice =
leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null;
if (leAudioLeadCachedDevice == null) {
log("getPreferredMainDevice: The LeadDevice is not in the all of devices list");
} else if (isDeviceConnected(leAudioLeadCachedDevice)) {
log("getPreferredMainDevice: The connected LeadDevice from LE profile");
return leAudioLeadCachedDevice;
}
CachedBluetoothDevice oneOfConnectedDevices =
groupDevicesList.stream()
.filter(cachedDevice -> isDeviceConnected(cachedDevice))
.findFirst()
.orElse(null);
if (oneOfConnectedDevices != null) {
log("getPreferredMainDevice: One of the connected devices.");
return oneOfConnectedDevices;
}
if (dualModeDevice != null) {
log("getPreferredMainDevice: The DUAL mode device.");
return dualModeDevice;
}
// last
if (!groupDevicesList.isEmpty()) {
log("getPreferredMainDevice: One of the group devices.");
return groupDevicesList.get(0);
}
return null;
}
@VisibleForTesting
boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) {
boolean hasChanged = false;
if (preferredMainDevice == null) {
log("addMemberDevicesIntoMainDevice: No main device. Do nothing.");
return hasChanged;
}
// If the current main device is not preferred main device, then set it as new main device.
// Otherwise, do nothing.
BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice();
CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice);
boolean hasPreferredMainDeviceAlreadyBeenMainDevice =
mainDeviceOfPreferredMainDevice == null;
if (!hasPreferredMainDeviceAlreadyBeenMainDevice) {
// preferredMainDevice has not been the main device.
// switch relationship between the mainDeviceOfPreferredMainDevice and
// PreferredMainDevice
log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. "
+ "Do switch relationship between the mainDeviceOfPreferredMainDevice and "
+ "PreferredMainDevice");
// To switch content and dispatch to notify UI change
mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice);
mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice);
mainDeviceOfPreferredMainDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice);
hasChanged = true;
}
// If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are
// the same groupId, then combine them and also keep the preferred main device as main
// device.
List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream()
.filter(device -> device.getGroupId() == groupId)
.collect(Collectors.toList());
boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1;
// Update the new main of CachedBluetoothDevice, since it may be changed in above step.
final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice);
if (haveMultiMainDevicesInAllOfDevicesList) {
// put another devices into main device.
for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) {
if (deviceItem.getDevice() == null || deviceItem.getDevice().equals(
bluetoothDeviceOfPreferredMainDevice)) {
continue;
}
Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice();
for (CachedBluetoothDevice memberSetItem : memberSet) {
if (!memberSetItem.equals(preferredMainDevice)) {
preferredMainDevice.addMemberDevice(memberSetItem);
}
}
memberSet.clear();
preferredMainDevice.addMemberDevice(deviceItem);
mCachedDevices.remove(deviceItem);
mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
hasChanged = true;
}
}
if (hasChanged) {
log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
+ mCachedDevices);
}
return hasChanged;
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
private boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) {
return false;
}
final BluetoothDevice device = cachedDevice.getDevice();
return cachedDevice.isConnected()
&& device.getBondState() == BluetoothDevice.BOND_BONDED
&& device.isConnected();
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2021 HIMSA II K/S - www.himsa.com.
* Represented by EHIMA - www.ehima.com
*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* CSIP Set Coordinator handles Bluetooth CSIP Set Coordinator role profile.
*/
public class CsipSetCoordinatorProfile implements LocalBluetoothProfile {
private static final String TAG = "CsipSetCoordinatorProfile";
private static final boolean VDBG = true;
private Context mContext;
private BluetoothCsipSetCoordinator mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "CSIP Set Coordinator";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class CoordinatedSetServiceListener implements BluetoothProfile.ServiceListener {
@RequiresApi(33)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (VDBG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothCsipSetCoordinator) proxy;
// We just bound to the service, so refresh the UI for any connected CSIP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (VDBG) {
Log.d(TAG, "CsipSetCoordinatorProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
CsipSetCoordinatorProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mDeviceManager.updateCsipDevices();
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (VDBG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
CsipSetCoordinatorProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new CoordinatedSetServiceListener(), BluetoothProfile.CSIP_SET_COORDINATOR);
}
/**
* Get CSIP devices matching connection states{
*
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Gets the connection status of the device.
*
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Connection status, {@code BluetoothProfile.STATE_DISCONNECTED} if unknown.
*/
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.CSIP_SET_COORDINATOR;
}
@Override
public boolean accessProfileEnabled() {
return false;
}
@Override
public boolean isAutoConnectable() {
return true;
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.summary_empty;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
return BluetoothUtils.getConnectionStateSummary(state);
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return 0;
}
/**
* Get the device's groups and correspondsing uuids map.
* @param device the bluetooth device
* @return Map of groups ids and related UUIDs
*/
public Map<Integer, ParcelUuid> getGroupUuidMapByDevice(BluetoothDevice device) {
if (mService == null || device == null) {
return null;
}
return mService.getGroupUuidMapByDevice(device);
}
/**
* Return the profile name as a string.
*/
public String toString() {
return NAME;
}
@RequiresApi(33)
protected void finalize() {
if (VDBG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.CSIP_SET_COORDINATOR, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up CSIP Set Coordinator proxy", t);
}
}
}
}

View File

@@ -0,0 +1,654 @@
/*
* Copyright (C) 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHapPresetInfo;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settingslib.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* HapClientProfile handles the Bluetooth HAP service client role.
*/
public class HapClientProfile implements LocalBluetoothProfile {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
HearingAidType.TYPE_INVALID,
HearingAidType.TYPE_BINAURAL,
HearingAidType.TYPE_MONAURAL,
HearingAidType.TYPE_BANDED,
HearingAidType.TYPE_RFU
})
/** Hearing aid type definition for HAP Client. */
public @interface HearingAidType {
int TYPE_INVALID = -1;
int TYPE_BINAURAL = BluetoothHapClient.TYPE_BINAURAL;
int TYPE_MONAURAL = BluetoothHapClient.TYPE_MONAURAL;
int TYPE_BANDED = BluetoothHapClient.TYPE_BANDED;
int TYPE_RFU = BluetoothHapClient.TYPE_RFU;
}
static final String NAME = "HapClient";
private static final String TAG = "HapClientProfile";
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private final BluetoothAdapter mBluetoothAdapter;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
private BluetoothHapClient mService;
private boolean mIsProfileReady;
// These callbacks run on the main thread.
private final class HapClientServiceListener implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHapClient) proxy;
// We just bound to the service, so refresh the UI for any connected HapClient devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// Adds a new device into mDeviceManager if it does not exist
if (device == null) {
Log.w(TAG, "HapClient profile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
HapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
// Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
@Override
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
mProfileManager.callServiceDisconnectedListeners();
}
}
HapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
if (bluetoothManager != null) {
mBluetoothAdapter = bluetoothManager.getAdapter();
mBluetoothAdapter.getProfileProxy(context, new HapClientServiceListener(),
BluetoothProfile.HAP_CLIENT);
} else {
mBluetoothAdapter = null;
}
}
/**
* Registers a {@link BluetoothHapClient.Callback} that will be invoked during the
* operation of this profile.
*
* Repeated registration of the same <var>callback</var> object after the first call to this
* method will result with IllegalArgumentException being thrown, even when the
* <var>executor</var> is different. API caller would have to call
* {@link #unregisterCallback(BluetoothHapClient.Callback)} with the same callback object
* before registering it again.
*
* @param executor an {@link Executor} to execute given callback
* @param callback user implementation of the {@link BluetoothHapClient.Callback}
* @throws NullPointerException if a null executor, or callback is given, or
* IllegalArgumentException if the same <var>callback</var> is already registered.
* @hide
*/
public void registerCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothHapClient.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
return;
}
mService.registerCallback(executor, callback);
}
/**
* Unregisters the specified {@link BluetoothHapClient.Callback}.
* <p>The same {@link BluetoothHapClient.Callback} object used when calling
* {@link #registerCallback(Executor, BluetoothHapClient.Callback)} must be used.
*
* <p>Callbacks are automatically unregistered when application process goes away
*
* @param callback user implementation of the {@link BluetoothHapClient.Callback}
* @throws NullPointerException when callback is null or IllegalArgumentException when no
* callback is registered
* @hide
*/
public void unregisterCallback(@NonNull BluetoothHapClient.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
return;
}
mService.unregisterCallback(callback);
}
/**
* Gets hearing aid devices matching connection states{
* {@code BluetoothProfile.STATE_CONNECTED},
* {@code BluetoothProfile.STATE_CONNECTING},
* {@code BluetoothProfile.STATE_DISCONNECTING}}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Gets hearing aid devices matching connection states{
* {@code BluetoothProfile.STATE_DISCONNECTED},
* {@code BluetoothProfile.STATE_CONNECTED},
* {@code BluetoothProfile.STATE_CONNECTING},
* {@code BluetoothProfile.STATE_DISCONNECTING}}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
/**
* Gets the hearing aid type of the device.
*
* @param device is the device for which we want to get the hearing aid type
* @return hearing aid type
*/
@HearingAidType
public int getHearingAidType(@NonNull BluetoothDevice device) {
if (mService == null) {
return HearingAidType.TYPE_INVALID;
}
return mService.getHearingAidType(device);
}
/**
* Gets if this device supports synchronized presets or not
*
* @param device is the device for which we want to know if supports synchronized presets
* @return {@code true} if the device supports synchronized presets
*/
public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsSynchronizedPresets(device);
}
/**
* Gets if this device supports independent presets or not
*
* @param device is the device for which we want to know if supports independent presets
* @return {@code true} if the device supports independent presets
*/
public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsIndependentPresets(device);
}
/**
* Gets if this device supports dynamic presets or not
*
* @param device is the device for which we want to know if supports dynamic presets
* @return {@code true} if the device supports dynamic presets
*/
public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsDynamicPresets(device);
}
/**
* Gets if this device supports writable presets or not
*
* @param device is the device for which we want to know if supports writable presets
* @return {@code true} if the device supports writable presets
*/
public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsWritablePresets(device);
}
/**
* Gets the group identifier, which can be used in the group related part of the API.
*
* <p>Users are expected to get group identifier for each of the connected device to discover
* the device grouping. This allows them to make an informed decision which devices can be
* controlled by single group API call and which require individual device calls.
*
* <p>Note that some binaural HA devices may not support group operations, therefore are not
* considered a valid HAP group. In such case -1 is returned even if such device is a valid Le
* Audio Coordinated Set member.
*
* @param device is the device for which we want to get the hap group identifier
* @return valid group identifier or -1
* @hide
*/
public int getHapGroup(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get hap group.");
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
return mService.getHapGroup(device);
}
/**
* Gets the currently active preset for a HA device.
*
* @param device is the device for which we want to set the active preset
* @return active preset index or {@link BluetoothHapClient#PRESET_INDEX_UNAVAILABLE} if the
* device is not connected.
* @hide
*/
public int getActivePresetIndex(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get active preset index.");
return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
}
return mService.getActivePresetIndex(device);
}
/**
* Gets the currently active preset info for a remote device.
*
* @param device is the device for which we want to get the preset name
* @return currently active preset info if selected, null if preset info is not available for
* the remote device
* @hide
*/
@Nullable
public BluetoothHapPresetInfo getActivePresetInfo(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get active preset info.");
return null;
}
return mService.getActivePresetInfo(device);
}
/**
* Selects the currently active preset for a HA device
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetSelected(BluetoothDevice, int, int)} will be
* called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onPresetSelectionFailed(BluetoothDevice, int)} will be
* called.
*
* @param device is the device for which we want to set the active preset
* @param presetIndex is an index of one of the available presets
* @hide
*/
public void selectPreset(@NonNull BluetoothDevice device, int presetIndex) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot select preset.");
return;
}
mService.selectPreset(device, presetIndex);
}
/**
* Selects the currently active preset for a Hearing Aid device group.
*
* <p>This group call may replace multiple device calls if those are part of the valid HAS
* group. Note that binaural HA devices may or may not support group.
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetSelected(BluetoothDevice, int, int)} will be
* called for each device within the group with reason code
* {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onPresetSelectionForGroupFailed(int, int)} will be
* called for the group.
*
* @param groupId is the device group identifier for which want to set the active preset
* @param presetIndex is an index of one of the available presets
* @hide
*/
public void selectPresetForGroup(int groupId, int presetIndex) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot select preset for group.");
return;
}
mService.selectPresetForGroup(groupId, presetIndex);
}
/**
* Sets the next preset as a currently active preset for a HA device
*
* <p>Note that the meaning of 'next' is HA device implementation specific and does not
* necessarily mean a higher preset index.
*
* @param device is the device for which we want to set the active preset
* @hide
*/
public void switchToNextPreset(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to next preset.");
return;
}
mService.switchToNextPreset(device);
}
/**
* Sets the next preset as a currently active preset for a HA device group
*
* <p>Note that the meaning of 'next' is HA device implementation specific and does not
* necessarily mean a higher preset index.
*
* <p>This group call may replace multiple device calls if those are part of the valid HAS
* group. Note that binaural HA devices may or may not support group.
*
* @param groupId is the device group identifier for which want to set the active preset
* @hide
*/
public void switchToNextPresetForGroup(int groupId) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to next preset for group.");
return;
}
mService.switchToNextPresetForGroup(groupId);
}
/**
* Sets the previous preset as a currently active preset for a HA device.
*
* <p>Note that the meaning of 'previous' is HA device implementation specific and does not
* necessarily mean a lower preset index.
*
* @param device is the device for which we want to set the active preset
* @hide
*/
public void switchToPreviousPreset(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to previous preset.");
return;
}
mService.switchToPreviousPreset(device);
}
/**
* Sets the next preset as a currently active preset for a HA device group
*
* <p>Note that the meaning of 'next' is HA device implementation specific and does not
* necessarily mean a higher preset index.
*
* <p>This group call may replace multiple device calls if those are part of the valid HAS
* group. Note that binaural HA devices may or may not support group.
*
* @param groupId is the device group identifier for which want to set the active preset
* @hide
*/
public void switchToPreviousPresetForGroup(int groupId) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to previous preset for "
+ "group.");
return;
}
mService.switchToPreviousPresetForGroup(groupId);
}
/**
* Requests the preset info
*
* @param device is the device for which we want to get the preset name
* @param presetIndex is an index of one of the available presets
* @return preset info
* @hide
*/
public BluetoothHapPresetInfo getPresetInfo(@NonNull BluetoothDevice device, int presetIndex) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get preset info.");
return null;
}
return mService.getPresetInfo(device, presetIndex);
}
/**
* Gets all preset info for a particular device
*
* @param device is the device for which we want to get all presets info
* @return a list of all known preset info
* @hide
*/
@NonNull
public List<BluetoothHapPresetInfo> getAllPresetInfo(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get all preset info.");
return new ArrayList<>();
}
return mService.getAllPresetInfo(device);
}
/**
* Sets the preset name for a particular device
*
* <p>Note that the name length is restricted to 40 characters.
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a
* new name will be called and reason code
* {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onSetPresetNameFailed(BluetoothDevice, int)} will be
* called.
*
* @param device is the device for which we want to get the preset name
* @param presetIndex is an index of one of the available presets
* @param name is a new name for a preset, maximum length is 40 characters
* @hide
*/
public void setPresetName(@NonNull BluetoothDevice device, int presetIndex,
@NonNull String name) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set preset name.");
return;
}
mService.setPresetName(device, presetIndex, name);
}
/**
* Sets the name for a hearing aid preset.
*
* <p>Note that the name length is restricted to 40 characters.
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a
* new name will be called for each device within the group with reason code
* {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onSetPresetNameForGroupFailed(int, int)} will be invoked
*
* @param groupId is the device group identifier
* @param presetIndex is an index of one of the available presets
* @param name is a new name for a preset, maximum length is 40 characters
* @hide
*/
public void setPresetNameForGroup(int groupId, int presetIndex, @NonNull String name) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set preset name for group.");
return;
}
mService.setPresetNameForGroup(groupId, presetIndex, name);
}
@Override
public boolean accessProfileEnabled() {
return false;
}
@Override
public boolean isAutoConnectable() {
return true;
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HAP_CLIENT;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_hearing_aid;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_hearing_aid;
}
/**
* Gets the name of this class
*
* @return the name of this class
*/
public String toString() {
return NAME;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HAP_CLIENT, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up HAP Client proxy", t);
}
}
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* HeadsetProfile handles Bluetooth HFP and Headset profiles.
*/
public class HeadsetProfile implements LocalBluetoothProfile {
private static final String TAG = "HeadsetProfile";
private BluetoothHeadset mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.HSP,
BluetoothUuid.HFP,
};
static final String NAME = "HEADSET";
// Order of this profile in device profiles list
private static final int ORDINAL = 0;
// These callbacks run on the main thread.
private final class HeadsetServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHeadset) proxy;
// We just bound to the service, so refresh the UI for any connected HFP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HeadsetProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(HeadsetProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
mProfileManager.callServiceConnectedListeners();
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HEADSET;
}
HeadsetProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(context, new HeadsetServiceListener(),
BluetoothProfile.HEADSET);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_PHONE_CALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_PHONE_CALL);
}
public BluetoothDevice getActiveDevice() {
if (mBluetoothAdapter == null) {
return null;
}
final List<BluetoothDevice> activeDevices = mBluetoothAdapter
.getActiveDevices(BluetoothProfile.HEADSET);
return (activeDevices.size() > 0) ? activeDevices.get(0) : null;
}
public int getAudioState(BluetoothDevice device) {
if (mService == null) {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
return mService.getAudioState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_headset;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_headset_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_headset_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headset_hfp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HEADSET,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up HID proxy", t);
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2023 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.settingslib.bluetooth;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Constant values used to configure hearing aid audio routing.
*
* {@link HearingAidAudioRoutingHelper}
*/
public final class HearingAidAudioRoutingConstants {
public static final int[] CALL_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_PHONE
AudioAttributes.USAGE_VOICE_COMMUNICATION,
};
public static final int[] MEDIA_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_MEDIA, including USAGE_GAME, USAGE_ASSISTANT,
// USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, USAGE_ASSISTANCE_SONIFICATION
AudioAttributes.USAGE_MEDIA,
// Stands for STRATEGY_ACCESSIBILITY
AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
// Stands for STRATEGY_DTMF
AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
};
public static final int[] RINGTONE_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_SONIFICATION, including USAGE_ALARM
AudioAttributes.USAGE_NOTIFICATION_RINGTONE
};
public static final int[] NOTIFICATION_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_SONIFICATION_RESPECTFUL, including USAGE_NOTIFICATION_EVENT
AudioAttributes.USAGE_NOTIFICATION,
};
@Retention(RetentionPolicy.SOURCE)
@IntDef({
RoutingValue.AUTO,
RoutingValue.HEARING_DEVICE,
RoutingValue.DEVICE_SPEAKER,
})
public @interface RoutingValue {
int AUTO = 0;
int HEARING_DEVICE = 1;
int DEVICE_SPEAKER = 2;
}
public static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "");
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2023 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.settingslib.bluetooth;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioProductStrategy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A helper class to configure the routing strategy for hearing aids.
*/
public class HearingAidAudioRoutingHelper {
private final AudioManager mAudioManager;
public HearingAidAudioRoutingHelper(Context context) {
mAudioManager = context.getSystemService(AudioManager.class);
}
/**
* Gets the list of {@link AudioProductStrategy} referred by the given list of usage values
* defined in {@link AudioAttributes}
*/
public List<AudioProductStrategy> getSupportedStrategies(int[] attributeSdkUsageList) {
final List<AudioAttributes> audioAttrList = new ArrayList<>(attributeSdkUsageList.length);
for (int attributeSdkUsage : attributeSdkUsageList) {
audioAttrList.add(new AudioAttributes.Builder().setUsage(attributeSdkUsage).build());
}
final List<AudioProductStrategy> allStrategies = getAudioProductStrategies();
final List<AudioProductStrategy> supportedStrategies = new ArrayList<>();
for (AudioProductStrategy strategy : allStrategies) {
for (AudioAttributes audioAttr : audioAttrList) {
if (strategy.supportsAudioAttributes(audioAttr)) {
supportedStrategies.add(strategy);
}
}
}
return supportedStrategies.stream().distinct().collect(Collectors.toList());
}
/**
* Sets the preferred device for the given strategies.
*
* @param supportedStrategies A list of {@link AudioProductStrategy} used to configure audio
* routing
* @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio
* routing
* @param routingValue one of value defined in
* {@link HearingAidAudioRoutingConstants.RoutingValue}, denotes routing
* destination.
* @return {code true} if the routing value successfully configure
*/
public boolean setPreferredDeviceRoutingStrategies(
List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice,
@HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
boolean status;
switch (routingValue) {
case HearingAidAudioRoutingConstants.RoutingValue.AUTO:
status = removePreferredDeviceForStrategies(supportedStrategies);
return status;
case HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE:
status = removePreferredDeviceForStrategies(supportedStrategies);
status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice);
return status;
case HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER:
status = removePreferredDeviceForStrategies(supportedStrategies);
status &= setPreferredDeviceForStrategies(supportedStrategies,
HearingAidAudioRoutingConstants.DEVICE_SPEAKER_OUT);
return status;
default:
throw new IllegalArgumentException("Unexpected routingValue: " + routingValue);
}
}
/**
* Gets the matched hearing device {@link AudioDeviceAttributes} for {@code device}.
*
* <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} of {@code device}
*
* @param device the {@link CachedBluetoothDevice} need to be hearing aid device
* @return the requested AudioDeviceAttributes or {@code null} if not match
*/
@Nullable
public AudioDeviceAttributes getMatchedHearingDeviceAttributes(CachedBluetoothDevice device) {
if (device == null || !device.isHearingAidDevice()) {
return null;
}
AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo audioDevice : audioDevices) {
// ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET
if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID
|| audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
if (matchAddress(device, audioDevice)) {
return new AudioDeviceAttributes(audioDevice);
}
}
}
return null;
}
private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) {
final String audioDeviceAddress = audioDevice.getAddress();
final CachedBluetoothDevice subDevice = device.getSubDevice();
final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
return device.getAddress().equals(audioDeviceAddress)
|| (subDevice != null && subDevice.getAddress().equals(audioDeviceAddress))
|| (!memberDevices.isEmpty() && memberDevices.stream().anyMatch(
m -> m.getAddress().equals(audioDeviceAddress)));
}
private boolean setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies,
AudioDeviceAttributes audioDevice) {
boolean status = true;
for (AudioProductStrategy strategy : strategies) {
status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice);
}
return status;
}
private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) {
boolean status = true;
for (AudioProductStrategy strategy : strategies) {
if (mAudioManager.getPreferredDeviceForStrategy(strategy) != null) {
status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
}
}
return status;
}
@VisibleForTesting
public List<AudioProductStrategy> getAudioProductStrategies() {
return AudioManager.getAudioProductStrategies();
}
}

View File

@@ -0,0 +1,390 @@
/*
* Copyright (C) 2018 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.settingslib.bluetooth;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.ScanFilter;
import android.content.ContentResolver;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.audiopolicy.AudioProductStrategy;
import android.os.ParcelUuid;
import android.provider.Settings;
import android.util.FeatureFlagUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* HearingAidDeviceManager manages the set of remote HearingAid(ASHA) Bluetooth devices.
*/
public class HearingAidDeviceManager {
private static final String TAG = "HearingAidDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
private final ContentResolver mContentResolver;
private final Context mContext;
private final LocalBluetoothManager mBtManager;
private final List<CachedBluetoothDevice> mCachedDevices;
private final HearingAidAudioRoutingHelper mRoutingHelper;
HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> CachedDevices) {
mContext = context;
mContentResolver = context.getContentResolver();
mBtManager = localBtManager;
mCachedDevices = CachedDevices;
mRoutingHelper = new HearingAidAudioRoutingHelper(context);
}
@VisibleForTesting
HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) {
mContext = context;
mContentResolver = context.getContentResolver();
mBtManager = localBtManager;
mCachedDevices = cachedDevices;
mRoutingHelper = routingHelper;
}
void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice,
List<ScanFilter> leScanFilters) {
HearingAidInfo info = generateHearingAidInfo(newDevice);
if (info != null) {
newDevice.setHearingAidInfo(info);
} else if (leScanFilters != null && !newDevice.isHearingAidDevice()) {
// If the device is added with hearing aid scan filter during pairing, set an empty
// hearing aid info to indicate it's a hearing aid device. The info will be updated
// when corresponding profiles connected.
for (ScanFilter leScanFilter: leScanFilters) {
final ParcelUuid serviceUuid = leScanFilter.getServiceUuid();
final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid();
if (BluetoothUuid.HEARING_AID.equals(serviceUuid)
|| BluetoothUuid.HAS.equals(serviceUuid)
|| BluetoothUuid.HEARING_AID.equals(serviceDataUuid)
|| BluetoothUuid.HAS.equals(serviceDataUuid)) {
newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
break;
}
}
}
}
boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) {
final long hiSyncId = newDevice.getHiSyncId();
if (isValidHiSyncId(hiSyncId)) {
final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId);
// Just add one of the hearing aids from a pair in the list that is shown in the UI.
// Once there is another device with the same hiSyncId, to add new device as sub
// device.
if (hearingAidDevice != null) {
hearingAidDevice.setSubDevice(newDevice);
return true;
}
}
return false;
}
private boolean isValidHiSyncId(long hiSyncId) {
return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
}
private CachedBluetoothDevice getCachedDevice(long hiSyncId) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getHiSyncId() == hiSyncId) {
return cachedDevice;
}
}
return null;
}
// To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId
void updateHearingAidsDevices() {
final Set<Long> newSyncIdSet = new HashSet<>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
// Do nothing if HiSyncId has been assigned
if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
continue;
}
HearingAidInfo info = generateHearingAidInfo(cachedDevice);
if (info != null) {
cachedDevice.setHearingAidInfo(info);
if (isValidHiSyncId(info.getHiSyncId())) {
newSyncIdSet.add(info.getHiSyncId());
}
}
}
for (Long syncId : newSyncIdSet) {
onHiSyncIdChanged(syncId);
}
}
// Group devices by hiSyncId
@VisibleForTesting
void onHiSyncIdChanged(long hiSyncId) {
int firstMatchedIndex = -1;
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getHiSyncId() != hiSyncId) {
continue;
}
// The remote device supports CSIP, the other ear should be processed as a member
// device. Ignore hiSyncId grouping from ASHA here.
if (cachedDevice.getProfiles().stream().anyMatch(
profile -> profile instanceof CsipSetCoordinatorProfile)) {
continue;
}
if (firstMatchedIndex == -1) {
// Found the first one
firstMatchedIndex = i;
continue;
}
// Found the second one
int indexToRemoveFromUi;
CachedBluetoothDevice subDevice;
CachedBluetoothDevice mainDevice;
// Since the hiSyncIds have been updated for a connected pair of hearing aids,
// we remove the entry of one the hearing aids from the UI. Unless the
// hiSyncId get updated, the system does not know it is a hearing aid, so we add
// both the hearing aids as separate entries in the UI first, then remove one
// of them after the hiSyncId is populated. We will choose the device that
// is not connected to be removed.
if (cachedDevice.isConnected()) {
mainDevice = cachedDevice;
indexToRemoveFromUi = firstMatchedIndex;
subDevice = mCachedDevices.get(firstMatchedIndex);
} else {
mainDevice = mCachedDevices.get(firstMatchedIndex);
indexToRemoveFromUi = i;
subDevice = cachedDevice;
}
mainDevice.setSubDevice(subDevice);
mCachedDevices.remove(indexToRemoveFromUi);
log("onHiSyncIdChanged: removed from UI device =" + subDevice
+ ", with hiSyncId=" + hiSyncId);
mBtManager.getEventManager().dispatchDeviceRemoved(subDevice);
break;
}
}
// @return {@code true}, the event is processed inside the method. It is for updating
// hearing aid device on main-sub relationship when receiving connected or disconnected.
// @return {@code false}, it is not hearing aid device or to process it same as other profiles
boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
int state) {
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
onHiSyncIdChanged(cachedDevice.getHiSyncId());
CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
if (mainDevice != null) {
if (mainDevice.isConnected()) {
// When main device exists and in connected state, receiving sub device
// connection. To refresh main device UI
mainDevice.refresh();
} else {
// When both Hearing Aid devices are disconnected, receiving sub device
// connection. To switch content and dispatch to notify UI change
mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
mainDevice.switchSubDeviceContent();
mainDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
}
return true;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
mainDevice = findMainDevice(cachedDevice);
if (cachedDevice.getUnpairing()) {
return true;
}
if (mainDevice != null) {
// When main device exists, receiving sub device disconnection
// To update main device UI
mainDevice.refresh();
return true;
}
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.isConnected()) {
// Main device is disconnected and sub device is connected
// To copy data from sub device to main device
mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
cachedDevice.switchSubDeviceContent();
cachedDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
return true;
}
break;
}
return false;
}
void onActiveDeviceChanged(CachedBluetoothDevice device) {
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice(
BluetoothProfile.LE_AUDIO)) {
setAudioRoutingConfig(device);
} else {
clearAudioRoutingConfig();
}
}
}
private void setAudioRoutingConfig(CachedBluetoothDevice device) {
AudioDeviceAttributes hearingDeviceAttributes =
mRoutingHelper.getMatchedHearingDeviceAttributes(device);
if (hearingDeviceAttributes == null) {
Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: "
+ device.getDevice().getAnonymizedAddress());
return;
}
final int callRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_CALL_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_MEDIA_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, callRoutingValue);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, mediaRoutingValue);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, ringtoneRoutingValue);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, systemSoundsRoutingValue);
}
private void clearAudioRoutingConfig() {
// Don't need to pass hearingDevice when we want to reset it (set to AUTO).
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
}
private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList,
AudioDeviceAttributes hearingDevice,
@HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
final List<AudioProductStrategy> supportedStrategies =
mRoutingHelper.getSupportedStrategies(attributeSdkUsageList);
final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies(
supportedStrategies, hearingDevice, routingValue);
if (!status) {
Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: "
+ routingValue + " fail to configure AudioProductStrategy");
}
}
CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.equals(device)) {
return cachedDevice;
}
}
}
return null;
}
private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final HearingAidProfile asha = profileManager.getHearingAidProfile();
if (asha == null) {
Log.w(TAG, "HearingAidProfile is not supported on this device");
} else {
long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice());
if (isValidHiSyncId(hiSyncId)) {
final HearingAidInfo info = new HearingAidInfo.Builder()
.setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice()))
.setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice()))
.setHiSyncId(hiSyncId)
.build();
if (DEBUG) {
Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info);
}
return info;
}
}
final HapClientProfile hapClientProfile = profileManager.getHapClientProfile();
final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
if (hapClientProfile == null || leAudioProfile == null) {
Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device");
} else if (cachedDevice.getProfiles().stream().anyMatch(
p -> p instanceof HapClientProfile)) {
int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice());
if (audioLocation != BluetoothLeAudio.AUDIO_LOCATION_INVALID
&& hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) {
final HearingAidInfo info = new HearingAidInfo.Builder()
.setLeAudioLocation(audioLocation)
.setHapDeviceType(hearingAidType)
.build();
if (DEBUG) {
Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info);
}
return info;
}
}
return null;
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright (C) 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.settingslib.bluetooth;
import android.annotation.IntDef;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.util.SparseIntArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/** Hearing aids information and constants that shared within hearing aids related profiles */
public class HearingAidInfo {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DeviceSide.SIDE_INVALID,
DeviceSide.SIDE_LEFT,
DeviceSide.SIDE_RIGHT,
DeviceSide.SIDE_LEFT_AND_RIGHT,
})
/** Side definition for hearing aids. */
public @interface DeviceSide {
int SIDE_INVALID = -1;
int SIDE_LEFT = 0;
int SIDE_RIGHT = 1;
int SIDE_LEFT_AND_RIGHT = 2;
}
@Retention(java.lang.annotation.RetentionPolicy.SOURCE)
@IntDef({
DeviceMode.MODE_INVALID,
DeviceMode.MODE_MONAURAL,
DeviceMode.MODE_BINAURAL,
DeviceMode.MODE_BANDED,
})
/** Mode definition for hearing aids. */
public @interface DeviceMode {
int MODE_INVALID = -1;
int MODE_MONAURAL = 0;
int MODE_BINAURAL = 1;
int MODE_BANDED = 2;
}
private final int mSide;
private final int mMode;
private final long mHiSyncId;
private HearingAidInfo(int side, int mode, long hiSyncId) {
mSide = side;
mMode = mode;
mHiSyncId = hiSyncId;
}
@DeviceSide
public int getSide() {
return mSide;
}
@DeviceMode
public int getMode() {
return mMode;
}
public long getHiSyncId() {
return mHiSyncId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof HearingAidInfo)) {
return false;
}
HearingAidInfo that = (HearingAidInfo) o;
return mSide == that.mSide && mMode == that.mMode && mHiSyncId == that.mHiSyncId;
}
@Override
public int hashCode() {
return Objects.hash(mSide, mMode, mHiSyncId);
}
@Override
public String toString() {
return "HearingAidInfo{"
+ "mSide=" + mSide
+ ", mMode=" + mMode
+ ", mHiSyncId=" + mHiSyncId
+ '}';
}
@DeviceSide
private static int convertAshaDeviceSideToInternalSide(int ashaDeviceSide) {
return ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.get(
ashaDeviceSide, DeviceSide.SIDE_INVALID);
}
@DeviceMode
private static int convertAshaDeviceModeToInternalMode(int ashaDeviceMode) {
return ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.get(
ashaDeviceMode, DeviceMode.MODE_INVALID);
}
@DeviceSide
private static int convertLeAudioLocationToInternalSide(int leAudioLocation) {
boolean isLeft = (leAudioLocation & LE_AUDIO_LOCATION_LEFT) != 0;
boolean isRight = (leAudioLocation & LE_AUDIO_LOCATION_RIGHT) != 0;
if (isLeft && isRight) {
return DeviceSide.SIDE_LEFT_AND_RIGHT;
} else if (isLeft) {
return DeviceSide.SIDE_LEFT;
} else if (isRight) {
return DeviceSide.SIDE_RIGHT;
}
return DeviceSide.SIDE_INVALID;
}
@DeviceMode
private static int convertHapDeviceTypeToInternalMode(int hapDeviceType) {
return HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.get(hapDeviceType, DeviceMode.MODE_INVALID);
}
/** Builder class for constructing {@link HearingAidInfo} objects. */
public static final class Builder {
private int mSide = DeviceSide.SIDE_INVALID;
private int mMode = DeviceMode.MODE_INVALID;
private long mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
/**
* Configure the hearing device mode.
* @param ashaDeviceMode one of the hearing aid device modes defined in HearingAidProfile
* {@link HearingAidProfile.DeviceMode}
*/
public Builder setAshaDeviceMode(int ashaDeviceMode) {
mMode = convertAshaDeviceModeToInternalMode(ashaDeviceMode);
return this;
}
/**
* Configure the hearing device mode.
* @param hapDeviceType one of the hearing aid device types defined in HapClientProfile
* {@link HapClientProfile.HearingAidType}
*/
public Builder setHapDeviceType(int hapDeviceType) {
mMode = convertHapDeviceTypeToInternalMode(hapDeviceType);
return this;
}
/**
* Configure the hearing device side.
* @param ashaDeviceSide one of the hearing aid device sides defined in HearingAidProfile
* {@link HearingAidProfile.DeviceSide}
*/
public Builder setAshaDeviceSide(int ashaDeviceSide) {
mSide = convertAshaDeviceSideToInternalSide(ashaDeviceSide);
return this;
}
/**
* Configure the hearing device side.
* @param leAudioLocation one of the audio location defined in BluetoothLeAudio
* {@link BluetoothLeAudio.AudioLocation}
*/
public Builder setLeAudioLocation(int leAudioLocation) {
mSide = convertLeAudioLocationToInternalSide(leAudioLocation);
return this;
}
/**
* Configure the hearing aid hiSyncId.
* @param hiSyncId the ASHA hearing aid id
*/
public Builder setHiSyncId(long hiSyncId) {
mHiSyncId = hiSyncId;
return this;
}
/** Build the configured {@link HearingAidInfo} */
public HearingAidInfo build() {
return new HearingAidInfo(mSide, mMode, mHiSyncId);
}
}
private static final int LE_AUDIO_LOCATION_LEFT =
BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER
| BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE
| BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND;
private static final int LE_AUDIO_LOCATION_RIGHT =
BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER
| BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE
| BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND;
private static final SparseIntArray ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING;
private static final SparseIntArray ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING;
private static final SparseIntArray HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING;
static {
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING = new SparseIntArray();
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
HearingAidProfile.DeviceSide.SIDE_INVALID, DeviceSide.SIDE_INVALID);
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
HearingAidProfile.DeviceSide.SIDE_LEFT, DeviceSide.SIDE_LEFT);
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
HearingAidProfile.DeviceSide.SIDE_RIGHT, DeviceSide.SIDE_RIGHT);
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING = new SparseIntArray();
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
HearingAidProfile.DeviceMode.MODE_INVALID, DeviceMode.MODE_INVALID);
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
HearingAidProfile.DeviceMode.MODE_MONAURAL, DeviceMode.MODE_MONAURAL);
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
HearingAidProfile.DeviceMode.MODE_BINAURAL, DeviceMode.MODE_BINAURAL);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING = new SparseIntArray();
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_INVALID, DeviceMode.MODE_INVALID);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_BINAURAL, DeviceMode.MODE_BINAURAL);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_MONAURAL, DeviceMode.MODE_MONAURAL);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_BANDED, DeviceMode.MODE_BANDED);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_RFU, DeviceMode.MODE_INVALID);
}
}

View File

@@ -0,0 +1,350 @@
/*
* Copyright (C) 2018 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import com.android.settingslib.R;
import com.android.settingslib.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
public class HearingAidProfile implements LocalBluetoothProfile {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DeviceSide.SIDE_INVALID,
DeviceSide.SIDE_LEFT,
DeviceSide.SIDE_RIGHT
})
/** Side definition for hearing aids. See {@link BluetoothHearingAid}. */
public @interface DeviceSide {
int SIDE_INVALID = -1;
int SIDE_LEFT = 0;
int SIDE_RIGHT = 1;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DeviceMode.MODE_INVALID,
DeviceMode.MODE_MONAURAL,
DeviceMode.MODE_BINAURAL
})
/** Mode definition for hearing aids. See {@link BluetoothHearingAid}. */
public @interface DeviceMode {
int MODE_INVALID = -1;
int MODE_MONAURAL = 0;
int MODE_BINAURAL = 1;
}
private static final String TAG = "HearingAidProfile";
private static boolean V = true;
private Context mContext;
private BluetoothHearingAid mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "HearingAid";
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class HearingAidServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHearingAid) proxy;
// We just bound to the service, so refresh the UI for any connected HearingAid devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (V) {
Log.d(TAG, "HearingAidProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(HearingAidProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
// Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
mProfileManager.callServiceDisconnectedListeners();
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HEARING_AID;
}
HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(context,
new HearingAidServiceListener(), BluetoothProfile.HEARING_AID);
}
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return true;
}
/**
* Get Hearing Aid devices matching connection states{
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Get Hearing Aid devices matching connection states{
* @code BluetoothProfile.STATE_DISCONNECTED,
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
int profiles = Utils.isAudioModeOngoingCall(mContext)
? ACTIVE_DEVICE_PHONE_CALL
: ACTIVE_DEVICE_AUDIO;
return device == null
? mBluetoothAdapter.removeActiveDevice(profiles)
: mBluetoothAdapter.setActiveDevice(device, profiles);
}
public List<BluetoothDevice> getActiveDevices() {
if (mBluetoothAdapter == null) {
return new ArrayList<>();
}
return mBluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
/**
* Tells remote device to set an absolute volume.
*
* @param volume Absolute volume to be set on remote
*/
public void setVolume(int volume) {
if (mService == null) {
return;
}
mService.setVolume(volume);
}
/**
* Gets the HiSyncId (unique hearing aid device identifier) of the device.
*
* @param device Bluetooth device
* @return the HiSyncId of the device
*/
public long getHiSyncId(BluetoothDevice device) {
if (mService == null || device == null) {
return BluetoothHearingAid.HI_SYNC_ID_INVALID;
}
return mService.getHiSyncId(device);
}
/**
* Gets the side of the device.
*
* @param device Bluetooth device.
* @return side of the device. See {@link DeviceSide}.
*/
@DeviceSide
public int getDeviceSide(@NonNull BluetoothDevice device) {
final int defaultValue = DeviceSide.SIDE_INVALID;
if (mService == null) {
Log.w(TAG, "Proxy not attached to HearingAidService");
return defaultValue;
}
return mService.getDeviceSide(device);
}
/**
* Gets the mode of the device.
*
* @param device Bluetooth device
* @return mode of the device. See {@link DeviceMode}.
*/
@DeviceMode
public int getDeviceMode(@NonNull BluetoothDevice device) {
final int defaultValue = DeviceMode.MODE_INVALID;
if (mService == null) {
Log.w(TAG, "Proxy not attached to HearingAidService");
return defaultValue;
}
return mService.getDeviceMode(device);
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_hearing_aid;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_hearing_aid;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HEARING_AID,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up Hearing Aid proxy", t);
}
}
}
}

View File

@@ -0,0 +1,360 @@
/*
* Copyright (C) 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.settingslib.bluetooth;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.stream.Collectors;
/** Utils class to report hearing aid metrics to statsd */
public final class HearingAidStatsLogUtils {
private static final String TAG = "HearingAidStatsLogUtils";
private static final boolean DEBUG = true;
private static final String ACCESSIBILITY_PREFERENCE = "accessibility_prefs";
private static final String BT_HEARING_AIDS_PAIRED_HISTORY = "bt_hearing_aids_paired_history";
private static final String BT_HEARING_AIDS_CONNECTED_HISTORY =
"bt_hearing_aids_connected_history";
private static final String BT_HEARING_DEVICES_PAIRED_HISTORY =
"bt_hearing_devices_paired_history";
private static final String BT_HEARING_DEVICES_CONNECTED_HISTORY =
"bt_hearing_devices_connected_history";
private static final String BT_HEARING_USER_CATEGORY = "bt_hearing_user_category";
private static final String HISTORY_RECORD_DELIMITER = ",";
static final String CATEGORY_HEARING_AIDS = "A11yHearingAidsUser";
static final String CATEGORY_NEW_HEARING_AIDS = "A11yNewHearingAidsUser";
static final String CATEGORY_HEARING_DEVICES = "A11yHearingDevicesUser";
static final String CATEGORY_NEW_HEARING_DEVICES = "A11yNewHearingDevicesUser";
static final int PAIRED_HISTORY_EXPIRED_DAY = 30;
static final int CONNECTED_HISTORY_EXPIRED_DAY = 7;
private static final int VALID_PAIRED_EVENT_COUNT = 1;
private static final int VALID_CONNECTED_EVENT_COUNT = 7;
/**
* Type of different Bluetooth device events history related to hearing.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
HistoryType.TYPE_UNKNOWN,
HistoryType.TYPE_HEARING_AIDS_PAIRED,
HistoryType.TYPE_HEARING_AIDS_CONNECTED,
HistoryType.TYPE_HEARING_DEVICES_PAIRED,
HistoryType.TYPE_HEARING_DEVICES_CONNECTED})
public @interface HistoryType {
int TYPE_UNKNOWN = -1;
int TYPE_HEARING_AIDS_PAIRED = 0;
int TYPE_HEARING_AIDS_CONNECTED = 1;
int TYPE_HEARING_DEVICES_PAIRED = 2;
int TYPE_HEARING_DEVICES_CONNECTED = 3;
}
private static final HashMap<String, Integer> sDeviceAddressToBondEntryMap = new HashMap<>();
private static final Set<String> sJustBondedDeviceAddressSet = new HashSet<>();
/**
* Sets the mapping from hearing aid device to the bond entry where this device starts it's
* bonding(connecting) process.
*
* @param bondEntry The entry page id where the bonding process starts
* @param device The bonding(connecting) hearing aid device
*/
public static void setBondEntryForDevice(int bondEntry, CachedBluetoothDevice device) {
sDeviceAddressToBondEntryMap.put(device.getAddress(), bondEntry);
}
/**
* Logs hearing aid device information to statsd, including device mode, device side, and entry
* page id where the binding(connecting) process starts.
*
* Only logs the info once after hearing aid is bonded(connected). Clears the map entry of this
* device when logging is completed.
*
* @param device The bonded(connected) hearing aid device
*/
public static void logHearingAidInfo(CachedBluetoothDevice device) {
final String deviceAddress = device.getAddress();
if (sDeviceAddressToBondEntryMap.containsKey(deviceAddress)) {
final int bondEntry = sDeviceAddressToBondEntryMap.getOrDefault(deviceAddress, -1);
final int deviceMode = device.getDeviceMode();
final int deviceSide = device.getDeviceSide();
FrameworkStatsLog.write(FrameworkStatsLog.HEARING_AID_INFO_REPORTED, deviceMode,
deviceSide, bondEntry);
sDeviceAddressToBondEntryMap.remove(deviceAddress);
} else {
Log.w(TAG, "The device address was not found. Hearing aid device info is not logged.");
}
}
@VisibleForTesting
static HashMap<String, Integer> getDeviceAddressToBondEntryMap() {
return sDeviceAddressToBondEntryMap;
}
/**
* Updates corresponding history if we found the device is a hearing device after profile state
* changed.
*
* @param context the request context
* @param cachedDevice the remote device
* @param profile the profile that has a state changed
* @param profileState the new profile state
*/
public static void updateHistoryIfNeeded(Context context, CachedBluetoothDevice cachedDevice,
LocalBluetoothProfile profile, int profileState) {
if (isJustBonded(cachedDevice.getAddress())) {
// Saves bonded timestamp as the source for judging whether to display
// the survey
if (cachedDevice.getProfiles().stream().anyMatch(
p -> (p instanceof HearingAidProfile || p instanceof HapClientProfile))) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_PAIRED);
} else if (cachedDevice.getProfiles().stream().anyMatch(
p -> (p instanceof A2dpSinkProfile || p instanceof HeadsetProfile))) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_PAIRED);
}
removeFromJustBonded(cachedDevice.getAddress());
}
// Saves connected timestamp as the source for judging whether to display
// the survey
if (profileState == BluetoothProfile.STATE_CONNECTED) {
if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_CONNECTED);
} else if (profile instanceof A2dpSinkProfile || profile instanceof HeadsetProfile) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
}
}
}
/**
* Returns the user category if the user is already categorized. Otherwise, checks the
* history and sees if the user is categorized as one of {@link #CATEGORY_HEARING_AIDS},
* {@link #CATEGORY_NEW_HEARING_AIDS}, {@link #CATEGORY_HEARING_DEVICES}, and
* {@link #CATEGORY_NEW_HEARING_DEVICES}.
*
* @param context the request context
* @return the category which user belongs to
*/
public static synchronized String getUserCategory(Context context) {
String userCategory = getSharedPreferences(context).getString(BT_HEARING_USER_CATEGORY, "");
if (!userCategory.isEmpty()) {
return userCategory;
}
LinkedList<Long> hearingAidsConnectedHistory = getHistory(context,
HistoryType.TYPE_HEARING_AIDS_CONNECTED);
if (hearingAidsConnectedHistory != null
&& hearingAidsConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
LinkedList<Long> hearingAidsPairedHistory = getHistory(context,
HistoryType.TYPE_HEARING_AIDS_PAIRED);
// Since paired history will be cleared after 30 days. If there's any record within 30
// days, the user will be categorized as CATEGORY_NEW_HEARING_AIDS. Otherwise, the user
// will be categorized as CATEGORY_HEARING_AIDS.
if (hearingAidsPairedHistory != null
&& hearingAidsPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
userCategory = CATEGORY_NEW_HEARING_AIDS;
} else {
userCategory = CATEGORY_HEARING_AIDS;
}
}
LinkedList<Long> hearingDevicesConnectedHistory = getHistory(context,
HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
if (hearingDevicesConnectedHistory != null
&& hearingDevicesConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
LinkedList<Long> hearingDevicesPairedHistory = getHistory(context,
HistoryType.TYPE_HEARING_DEVICES_PAIRED);
// Since paired history will be cleared after 30 days. If there's any record within 30
// days, the user will be categorized as CATEGORY_NEW_HEARING_DEVICES. Otherwise, the
// user will be categorized as CATEGORY_HEARING_DEVICES.
if (hearingDevicesPairedHistory != null
&& hearingDevicesPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
userCategory = CATEGORY_NEW_HEARING_DEVICES;
} else {
userCategory = CATEGORY_HEARING_DEVICES;
}
}
return userCategory;
}
/**
* Maintains a temporarily list of just bonded device address. After the device profiles are
* connected, {@link HearingAidStatsLogUtils#removeFromJustBonded} will be called to remove the
* address.
* @param address the device address
*/
public static void addToJustBonded(String address) {
sJustBondedDeviceAddressSet.add(address);
}
/**
* Removes the device address from the just bonded list.
* @param address the device address
*/
private static void removeFromJustBonded(String address) {
sJustBondedDeviceAddressSet.remove(address);
}
/**
* Checks whether the device address is in the just bonded list.
* @param address the device address
* @return true if the device address is in the just bonded list
*/
private static boolean isJustBonded(String address) {
return sJustBondedDeviceAddressSet.contains(address);
}
/**
* Adds current timestamp into BT hearing devices related history.
* @param context the request context
* @param type the type of history to store the data. See {@link HistoryType}.
*/
public static void addCurrentTimeToHistory(Context context, @HistoryType int type) {
addToHistory(context, type, System.currentTimeMillis());
}
static synchronized void addToHistory(Context context, @HistoryType int type,
long timestamp) {
LinkedList<Long> history = getHistory(context, type);
if (history == null) {
if (DEBUG) {
Log.w(TAG, "Couldn't find shared preference name matched type=" + type);
}
return;
}
if (history.peekLast() != null && isSameDay(timestamp, history.peekLast())) {
if (DEBUG) {
Log.w(TAG, "Skip this record, it's same day record");
}
return;
}
history.add(timestamp);
SharedPreferences.Editor editor = getSharedPreferences(context).edit();
editor.putString(HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type),
convertToHistoryString(history)).apply();
}
@Nullable
static synchronized LinkedList<Long> getHistory(Context context, @HistoryType int type) {
String spName = HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type);
if (BT_HEARING_AIDS_PAIRED_HISTORY.equals(spName)
|| BT_HEARING_DEVICES_PAIRED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDay(history, PAIRED_HISTORY_EXPIRED_DAY);
return history;
} else if (BT_HEARING_AIDS_CONNECTED_HISTORY.equals(spName)
|| BT_HEARING_DEVICES_CONNECTED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDay(history, CONNECTED_HISTORY_EXPIRED_DAY);
return history;
}
return null;
}
private static void removeRecordsBeforeDay(LinkedList<Long> history, int day) {
if (history == null || history.isEmpty()) {
return;
}
long currentTime = System.currentTimeMillis();
while (history.peekFirst() != null
&& dayDifference(currentTime, history.peekFirst()) >= day) {
history.poll();
}
}
private static String convertToHistoryString(LinkedList<Long> history) {
return history.stream().map(Object::toString).collect(
Collectors.joining(HISTORY_RECORD_DELIMITER));
}
private static LinkedList<Long> convertToHistoryList(String string) {
if (string == null || string.isEmpty()) {
return new LinkedList<>();
}
LinkedList<Long> ll = new LinkedList<>();
String[] elements = string.split(HISTORY_RECORD_DELIMITER);
for (String e: elements) {
if (e.isEmpty()) continue;
ll.offer(Long.parseLong(e));
}
return ll;
}
/**
* Check if two timestamps are in the same date according to current timezone. This function
* doesn't consider the original timezone when the timestamp is saved.
*
* @param t1 the first epoch timestamp
* @param t2 the second epoch timestamp
* @return {@code true} if two timestamps are on the same day
*/
private static boolean isSameDay(long t1, long t2) {
return dayDifference(t1, t2) == 0;
}
private static long dayDifference(long t1, long t2) {
ZoneId zoneId = ZoneId.systemDefault();
LocalDate date1 = Instant.ofEpochMilli(t1).atZone(zoneId).toLocalDate();
LocalDate date2 = Instant.ofEpochMilli(t2).atZone(zoneId).toLocalDate();
return Math.abs(ChronoUnit.DAYS.between(date1, date2));
}
private static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(ACCESSIBILITY_PREFERENCE, Context.MODE_PRIVATE);
}
private static final HashMap<Integer, String> HISTORY_TYPE_TO_SP_NAME_MAPPING;
static {
HISTORY_TYPE_TO_SP_NAME_MAPPING = new HashMap<>();
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_PAIRED, BT_HEARING_AIDS_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_CONNECTED, BT_HEARING_AIDS_CONNECTED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_DEVICES_PAIRED, BT_HEARING_DEVICES_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_DEVICES_CONNECTED, BT_HEARING_DEVICES_CONNECTED_HISTORY);
}
private HearingAidStatsLogUtils() {}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2016 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* Handles the Handsfree HF role.
*/
final class HfpClientProfile implements LocalBluetoothProfile {
private static final String TAG = "HfpClientProfile";
private BluetoothHeadsetClient mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final ParcelUuid[] SRC_UUIDS = {
BluetoothUuid.HSP_AG,
BluetoothUuid.HFP_AG,
};
static final String NAME = "HEADSET_CLIENT";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 0;
// These callbacks run on the main thread.
private final class HfpClientServiceListener
implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHeadsetClient) proxy;
// We just bound to the service, so refresh the UI for any connected HFP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HfpClient profile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
HfpClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
@Override
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HEADSET_CLIENT;
}
HfpClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new HfpClientServiceListener(), BluetoothProfile.HEADSET_CLIENT);
}
@Override
public boolean accessProfileEnabled() {
return true;
}
@Override
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public String toString() {
return NAME;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_headset;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_headset_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_headset_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headset_hfp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.HEADSET_CLIENT, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up HfpClient proxy", t);
}
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2017 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.R;
import java.util.List;
/**
* HidDeviceProfile handles Bluetooth HID Device role
*/
public class HidDeviceProfile implements LocalBluetoothProfile {
private static final String TAG = "HidDeviceProfile";
// Order of this profile in device profiles list
private static final int ORDINAL = 18;
// HID Device Profile is always preferred.
private static final int PREFERRED_VALUE = -1;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final String NAME = "HID DEVICE";
private BluetoothHidDevice mService;
private boolean mIsProfileReady;
HidDeviceProfile(Context context,CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new HidDeviceServiceListener(), BluetoothProfile.HID_DEVICE);
}
// These callbacks run on the main thread.
private final class HidDeviceServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHidDevice) proxy;
// We just bound to the service, so refresh the UI for any connected HID devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
for (BluetoothDevice nextDevice : deviceList) {
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HidProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
Log.d(TAG, "Connection status changed: " + device);
device.onProfileStateChanged(HidDeviceProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
}
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HID_DEVICE;
}
@Override
public boolean accessProfileEnabled() {
return true;
}
@Override
public boolean isAutoConnectable() {
return false;
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return getConnectionStatus(device) != BluetoothProfile.STATE_DISCONNECTED;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return PREFERRED_VALUE;
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
// if set preferred to false, then disconnect to the current device
if (!enabled) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public String toString() {
return NAME;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_hid;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
final int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_misc_hid;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HID_DEVICE,
mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up HID proxy", t);
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.R;
import java.util.List;
/**
* HidProfile handles Bluetooth HID Host role.
*/
public class HidProfile implements LocalBluetoothProfile {
private static final String TAG = "HidProfile";
private BluetoothHidHost mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final String NAME = "HID";
// Order of this profile in device profiles list
private static final int ORDINAL = 3;
// These callbacks run on the main thread.
private final class HidHostServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHidHost) proxy;
// We just bound to the service, so refresh the UI for any connected HID devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HidProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(HidProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HID_HOST;
}
HidProfile(Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new HidHostServiceListener(),
BluetoothProfile.HID_HOST);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) != CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
// TODO: distinguish between keyboard and mouse?
return R.string.bluetooth_profile_hid;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
if (btClass == null) {
return com.android.internal.R.drawable.ic_lockscreen_ime;
}
return getHidClassDrawable(btClass);
}
public static int getHidClassDrawable(BluetoothClass btClass) {
switch (btClass.getDeviceClass()) {
case BluetoothClass.Device.PERIPHERAL_KEYBOARD:
case BluetoothClass.Device.PERIPHERAL_KEYBOARD_POINTING:
return com.android.internal.R.drawable.ic_lockscreen_ime;
case BluetoothClass.Device.PERIPHERAL_POINTING:
return com.android.internal.R.drawable.ic_bt_pointing_hid;
default:
return com.android.internal.R.drawable.ic_bt_misc_hid;
}
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HID_HOST,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up HID proxy", t);
}
}
}
}

View File

@@ -0,0 +1,335 @@
/* Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA
- www.ehima.com
*/
/* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
public class LeAudioProfile implements LocalBluetoothProfile {
private static final String TAG = "LeAudioProfile";
private static boolean DEBUG = true;
private Context mContext;
private BluetoothLeAudio mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "LE_AUDIO";
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class LeAudioServiceListener implements BluetoothProfile.ServiceListener {
@RequiresApi(Build.VERSION_CODES.S)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothLeAudio) proxy;
// We just bound to the service, so refresh the UI for any connected LeAudio devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(LeAudioProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
// Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.LE_AUDIO;
}
LeAudioProfile(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(
context, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(
new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(
new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
/*
* @hide
*/
public boolean connect(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
}
/*
* @hide
*/
public boolean disconnect(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
/** Get group id for {@link BluetoothDevice}. */
public int getGroupId(@NonNull BluetoothDevice device) {
if (mService == null) {
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
return mService.getGroupId(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
}
public List<BluetoothDevice> getActiveDevices() {
if (mBluetoothAdapter == null) {
return new ArrayList<>();
}
return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
}
/**
* Get Lead device for the group.
*
* <p>Lead device is the device that can be used as an active device in the system. Active
* devices points to the Audio Device for the Le Audio group. This method returns the Lead
* devices for the connected LE Audio group and this device should be used in the
* setActiveDevice() method by other parts of the system, which wants to set to active a
* particular Le Audio group.
*
* <p>Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
* Note: When Lead device gets disconnected while Le Audio group is active and has more devices
* in the group, then Lead device will not change. If Lead device gets disconnected, for the Le
* Audio group which is not active, a new Lead device will be chosen
*
* @param groupId The group id.
* @return group lead device.
* @hide
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
if (DEBUG) {
Log.d(TAG, "getConnectedGroupLeadDevice");
}
if (mService == null) {
Log.e(TAG, "No service.");
return null;
}
return mService.getConnectedGroupLeadDevice(groupId);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_le_audio;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_le_audio_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_le_audio_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
if (btClass == null) {
Log.e(TAG, "No btClass.");
return R.drawable.ic_bt_le_audio_speakers;
}
switch (btClass.getDeviceClass()) {
case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
return R.drawable.ic_bt_le_audio;
default:
return R.drawable.ic_bt_le_audio_speakers;
}
}
public int getAudioLocation(BluetoothDevice device) {
if (mService == null || device == null) {
return BluetoothLeAudio.AUDIO_LOCATION_INVALID;
}
return mService.getAudioLocation(device);
}
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter()
.closeProfileProxy(BluetoothProfile.LE_AUDIO, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up LeAudio proxy", t);
}
}
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.le.BluetoothLeScanner;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import java.time.Duration;
import java.util.List;
import java.util.Set;
/**
* LocalBluetoothAdapter provides an interface between the Settings app
* and the functionality of the local {@link BluetoothAdapter}, specifically
* those related to state transitions of the adapter itself.
*
* <p>Connection and bonding state changes affecting specific devices
* are handled by {@link CachedBluetoothDeviceManager},
* {@link BluetoothEventManager}, and {@link LocalBluetoothProfileManager}.
*
* @deprecated use {@link BluetoothAdapter} instead.
*/
@Deprecated
public class LocalBluetoothAdapter {
private static final String TAG = "LocalBluetoothAdapter";
/** This class does not allow direct access to the BluetoothAdapter. */
private final BluetoothAdapter mAdapter;
private LocalBluetoothProfileManager mProfileManager;
private static LocalBluetoothAdapter sInstance;
private int mState = BluetoothAdapter.ERROR;
private static final int SCAN_EXPIRATION_MS = 5 * 60 * 1000; // 5 mins
private long mLastScan;
private LocalBluetoothAdapter(BluetoothAdapter adapter) {
mAdapter = adapter;
}
void setProfileManager(LocalBluetoothProfileManager manager) {
mProfileManager = manager;
}
/**
* Get the singleton instance of the LocalBluetoothAdapter. If this device
* doesn't support Bluetooth, then null will be returned. Callers must be
* prepared to handle a null return value.
* @return the LocalBluetoothAdapter object, or null if not supported
*/
static synchronized LocalBluetoothAdapter getInstance() {
if (sInstance == null) {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
sInstance = new LocalBluetoothAdapter(adapter);
}
}
return sInstance;
}
// Pass-through BluetoothAdapter methods that we can intercept if necessary
public void cancelDiscovery() {
mAdapter.cancelDiscovery();
}
public boolean enable() {
return mAdapter.enable();
}
public boolean disable() {
return mAdapter.disable();
}
public String getAddress() {
return mAdapter.getAddress();
}
void getProfileProxy(Context context,
BluetoothProfile.ServiceListener listener, int profile) {
mAdapter.getProfileProxy(context, listener, profile);
}
public Set<BluetoothDevice> getBondedDevices() {
return mAdapter.getBondedDevices();
}
public String getName() {
return mAdapter.getName();
}
public int getScanMode() {
return mAdapter.getScanMode();
}
public BluetoothLeScanner getBluetoothLeScanner() {
return mAdapter.getBluetoothLeScanner();
}
public int getState() {
return mAdapter.getState();
}
public ParcelUuid[] getUuids() {
List<ParcelUuid> uuidsList = mAdapter.getUuidsList();
ParcelUuid[] uuidsArray = new ParcelUuid[uuidsList.size()];
uuidsList.toArray(uuidsArray);
return uuidsArray;
}
public boolean isDiscovering() {
return mAdapter.isDiscovering();
}
public boolean isEnabled() {
return mAdapter.isEnabled();
}
public int getConnectionState() {
return mAdapter.getConnectionState();
}
public void setDiscoverableTimeout(int timeout) {
mAdapter.setDiscoverableTimeout(Duration.ofSeconds(timeout));
}
public long getDiscoveryEndMillis() {
return mAdapter.getDiscoveryEndMillis();
}
public void setName(String name) {
mAdapter.setName(name);
}
public void setScanMode(int mode) {
mAdapter.setScanMode(mode);
}
public boolean setScanMode(int mode, int duration) {
return (mAdapter.setDiscoverableTimeout(Duration.ofSeconds(duration))
== BluetoothStatusCodes.SUCCESS
&& mAdapter.setScanMode(mode) == BluetoothStatusCodes.SUCCESS);
}
public void startScanning(boolean force) {
// Only start if we're not already scanning
if (!mAdapter.isDiscovering()) {
if (!force) {
// Don't scan more than frequently than SCAN_EXPIRATION_MS,
// unless forced
if (mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) {
return;
}
// If we are playing music, don't scan unless forced.
A2dpProfile a2dp = mProfileManager.getA2dpProfile();
if (a2dp != null && a2dp.isA2dpPlaying()) {
return;
}
A2dpSinkProfile a2dpSink = mProfileManager.getA2dpSinkProfile();
if ((a2dpSink != null) && (a2dpSink.isAudioPlaying())) {
return;
}
}
if (mAdapter.startDiscovery()) {
mLastScan = System.currentTimeMillis();
}
}
}
public void stopScanning() {
if (mAdapter.isDiscovering()) {
mAdapter.cancelDiscovery();
}
}
public synchronized int getBluetoothState() {
// Always sync state, in case it changed while paused
syncBluetoothState();
return mState;
}
void setBluetoothStateInt(int state) {
synchronized(this) {
if (mState == state) {
return;
}
mState = state;
}
if (state == BluetoothAdapter.STATE_ON) {
// if mProfileManager hasn't been constructed yet, it will
// get the adapter UUIDs in its constructor when it is.
if (mProfileManager != null) {
mProfileManager.setBluetoothStateOn();
}
}
}
// Returns true if the state changed; false otherwise.
boolean syncBluetoothState() {
int currentState = mAdapter.getState();
if (currentState != mState) {
setBluetoothStateInt(mAdapter.getState());
return true;
}
return false;
}
public boolean setBluetoothEnabled(boolean enabled) {
boolean success = enabled
? mAdapter.enable()
: mAdapter.disable();
if (success) {
setBluetoothStateInt(enabled
? BluetoothAdapter.STATE_TURNING_ON
: BluetoothAdapter.STATE_TURNING_OFF);
} else {
if (BluetoothUtils.V) {
Log.v(TAG, "setBluetoothEnabled call, manager didn't return " +
"success for enabled: " + enabled);
}
syncBluetoothState();
}
return success;
}
public BluetoothDevice getRemoteDevice(String address) {
return mAdapter.getRemoteDevice(address);
}
public List<Integer> getSupportedProfiles() {
return mAdapter.getSupportedProfiles();
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 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.settingslib.bluetooth;
import android.bluetooth.BluetoothLeAudioContentMetadata;
public class LocalBluetoothLeAudioContentMetadata {
private static final String TAG = "LocalBluetoothLeAudioContentMetadata";
private final BluetoothLeAudioContentMetadata mContentMetadata;
private final String mLanguage;
private final byte[] mRawMetadata;
private String mProgramInfo;
LocalBluetoothLeAudioContentMetadata(BluetoothLeAudioContentMetadata contentMetadata) {
mContentMetadata = contentMetadata;
mProgramInfo = contentMetadata.getProgramInfo();
mLanguage = contentMetadata.getLanguage();
mRawMetadata = contentMetadata.getRawMetadata();
}
public void setProgramInfo(String programInfo) {
mProgramInfo = programInfo;
}
public String getProgramInfo() {
return mProgramInfo;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,450 @@
/*
* Copyright (C) 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProfile.ServiceListener;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
* LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the
* functionality of the local {@link BluetoothLeBroadcastAssistant}. Use the {@link
* BluetoothLeBroadcastAssistant.Callback} to get the result callback.
*/
public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile {
private static final String TAG = "LocalBluetoothLeBroadcastAssistant";
private static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
private static final boolean DEBUG = BluetoothUtils.D;
static final String NAME = "LE_AUDIO_BROADCAST_ASSISTANT";
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private LocalBluetoothProfileManager mProfileManager;
private BluetoothLeBroadcastAssistant mService;
private final CachedBluetoothDeviceManager mDeviceManager;
private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
private BluetoothLeBroadcastMetadata.Builder mBuilder;
private boolean mIsProfileReady;
// Cached assistant callbacks being register before service is connected.
private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap =
new ConcurrentHashMap<>();
private final ServiceListener mServiceListener =
new ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothLeBroadcastAssistant) proxy;
// We just bound to the service, so refresh the UI for any connected LeAudio
// devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(
TAG,
"LocalBluetoothLeBroadcastAssistant found new device: "
+ nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
LocalBluetoothLeBroadcastAssistant.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
if (DEBUG) {
Log.d(
TAG,
"onServiceConnected, register mCachedCallbackExecutorMap = "
+ mCachedCallbackExecutorMap);
}
mCachedCallbackExecutorMap.forEach(
(callback, executor) -> registerServiceCallBack(executor, callback));
}
@Override
public void onServiceDisconnected(int profile) {
if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT");
return;
}
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
mCachedCallbackExecutorMap.clear();
}
};
public LocalBluetoothLeBroadcastAssistant(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mProfileManager = profileManager;
mDeviceManager = deviceManager;
BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(
context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
mBuilder = new BluetoothLeBroadcastMetadata.Builder();
}
/**
* Add a Broadcast Source to the Broadcast Sink with {@link BluetoothLeBroadcastMetadata}.
*
* @param sink Broadcast Sink to which the Broadcast Source should be added
* @param metadata Broadcast Source metadata to be added to the Broadcast Sink
* @param isGroupOp {@code true} if Application wants to perform this operation for all
* coordinated set members throughout this session. Otherwise, caller would have to add,
* modify, and remove individual set members.
*/
public void addSource(
BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.addSource(sink, metadata, isGroupOp);
}
/**
* Add a Broadcast Source to the Broadcast Sink with the information which are separated from
* the qr code string.
*
* @param sink Broadcast Sink to which the Broadcast Source should be added
* @param sourceAddressType hardware MAC Address of the device. See {@link
* BluetoothDevice.AddressType}.
* @param presentationDelayMicros presentation delay of this Broadcast Source in microseconds.
* @param sourceAdvertisingSid 1-byte long Advertising_SID of the Broadcast Source.
* @param broadcastId 3-byte long Broadcast_ID of the Broadcast Source.
* @param paSyncInterval Periodic Advertising Sync interval of the broadcast Source, {@link
* BluetoothLeBroadcastMetadata#PA_SYNC_INTERVAL_UNKNOWN} if unknown.
* @param isEncrypted whether the Broadcast Source is encrypted.
* @param broadcastCode Broadcast Code for this Broadcast Source, null if code is not required.
* @param sourceDevice source advertiser address.
* @param isGroupOp {@code true} if Application wants to perform this operation for all
* coordinated set members throughout this session. Otherwise, caller would have to add,
* modify, and remove individual set members.
*/
public void addSource(
@NonNull BluetoothDevice sink,
int sourceAddressType,
int presentationDelayMicros,
int sourceAdvertisingSid,
int broadcastId,
int paSyncInterval,
boolean isEncrypted,
byte[] broadcastCode,
BluetoothDevice sourceDevice,
boolean isGroupOp) {
if (DEBUG) {
Log.d(TAG, "addSource()");
}
buildMetadata(
sourceAddressType,
presentationDelayMicros,
sourceAdvertisingSid,
broadcastId,
paSyncInterval,
isEncrypted,
broadcastCode,
sourceDevice);
addSource(sink, mBluetoothLeBroadcastMetadata, isGroupOp);
}
private void buildMetadata(
int sourceAddressType,
int presentationDelayMicros,
int sourceAdvertisingSid,
int broadcastId,
int paSyncInterval,
boolean isEncrypted,
byte[] broadcastCode,
BluetoothDevice sourceDevice) {
mBluetoothLeBroadcastMetadata =
mBuilder.setSourceDevice(sourceDevice, sourceAddressType)
.setSourceAdvertisingSid(sourceAdvertisingSid)
.setBroadcastId(broadcastId)
.setPaSyncInterval(paSyncInterval)
.setEncrypted(isEncrypted)
.setBroadcastCode(broadcastCode)
.setPresentationDelayMicros(presentationDelayMicros)
.build();
}
public void removeSource(@NonNull BluetoothDevice sink, int sourceId) {
if (DEBUG) {
Log.d(TAG, "removeSource()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.removeSource(sink, sourceId);
}
public void startSearchingForSources(@NonNull List<android.bluetooth.le.ScanFilter> filters) {
if (DEBUG) {
Log.d(TAG, "startSearchingForSources()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.startSearchingForSources(filters);
}
/**
* Return true if a search has been started by this application.
*
* @return true if a search has been started by this application
* @hide
*/
public boolean isSearchInProgress() {
if (DEBUG) {
Log.d(TAG, "isSearchInProgress()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return false;
}
return mService.isSearchInProgress();
}
/**
* Stops an ongoing search for nearby Broadcast Sources.
*
* <p>On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be
* called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure,
* {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be called with
* reason code
*
* @throws IllegalStateException if callback was not registered
*/
public void stopSearchingForSources() {
if (DEBUG) {
Log.d(TAG, "stopSearchingForSources()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.stopSearchingForSources();
}
/**
* Get information about all Broadcast Sources that a Broadcast Sink knows about.
*
* @param sink Broadcast Sink from which to get all Broadcast Sources
* @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} stored
* in the Broadcast Sink
* @throws NullPointerException when <var>sink</var> is null
*/
public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources(
@NonNull BluetoothDevice sink) {
if (DEBUG) {
Log.d(TAG, "getAllSources()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return new ArrayList<BluetoothLeBroadcastReceiveState>();
}
return mService.getAllSources(sink);
}
/**
* Register Broadcast Assistant Callbacks to track its state and receivers
*
* @param executor Executor object for callback
* @param callback Callback object to be registered
*/
public void registerServiceCallBack(
@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothLeBroadcastAssistant.Callback callback) {
if (mService == null) {
Log.d(
TAG,
"registerServiceCallBack failed, the BluetoothLeBroadcastAssistant is null.");
mCachedCallbackExecutorMap.putIfAbsent(callback, executor);
return;
}
try {
mService.registerCallback(executor, callback);
} catch (IllegalArgumentException e) {
Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage());
}
}
/**
* Unregister previously registered Broadcast Assistant Callbacks
*
* @param callback Callback object to be unregistered
*/
public void unregisterServiceCallBack(
@NonNull BluetoothLeBroadcastAssistant.Callback callback) {
mCachedCallbackExecutorMap.remove(callback);
if (mService == null) {
Log.d(
TAG,
"unregisterServiceCallBack failed, the BluetoothLeBroadcastAssistant is null.");
return;
}
try {
mService.unregisterCallback(callback);
} catch (IllegalArgumentException e) {
Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage());
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
public int getProfileId() {
return BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT;
}
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
// LE Audio Broadcasts are not connection-oriented.
return mService.getConnectionState(device);
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isEnabled = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isEnabled;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.summary_empty;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
return BluetoothUtils.getConnectionStateSummary(state);
}
public int getDrawableResource(BluetoothClass btClass) {
return 0;
}
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter()
.closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up LeAudio proxy", t);
}
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 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.settingslib.bluetooth
import android.bluetooth.BluetoothLeBroadcastMetadata
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt.toQrCodeString
@Deprecated("Replace with BluetoothLeBroadcastMetadataExt")
class LocalBluetoothLeBroadcastMetadata(private val metadata: BluetoothLeBroadcastMetadata?) {
constructor() : this(null)
fun convertToQrCodeString(): String = metadata?.toQrCodeString() ?: ""
fun convertToBroadcastMetadata(qrCodeString: String) =
BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(qrCodeString)
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import android.content.Context;
import android.os.Handler;
import android.os.UserHandle;
import android.util.Log;
import java.lang.ref.WeakReference;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
/**
* LocalBluetoothManager provides a simplified interface on top of a subset of
* the Bluetooth API. Note that {@link #getInstance} will return null
* if there is no Bluetooth adapter on this device, and callers must be
* prepared to handle this case.
*/
public class LocalBluetoothManager {
private static final String TAG = "LocalBluetoothManager";
/** Singleton instance. */
private static LocalBluetoothManager sInstance;
private final Context mContext;
/** If a BT-related activity is in the foreground, this will be it. */
private WeakReference<Context> mForegroundActivity;
private final LocalBluetoothAdapter mLocalAdapter;
private final CachedBluetoothDeviceManager mCachedDeviceManager;
/** The Bluetooth profile manager. */
private final LocalBluetoothProfileManager mProfileManager;
/** The broadcast receiver event manager. */
private final BluetoothEventManager mEventManager;
@Nullable
public static synchronized LocalBluetoothManager getInstance(Context context,
BluetoothManagerCallback onInitCallback) {
if (sInstance == null) {
LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance();
if (adapter == null) {
return null;
}
// This will be around as long as this process is
sInstance = new LocalBluetoothManager(adapter, context, /* handler= */ null,
/* userHandle= */ null);
if (onInitCallback != null) {
onInitCallback.onBluetoothManagerInitialized(context.getApplicationContext(),
sInstance);
}
}
return sInstance;
}
/**
* Returns a new instance of {@link LocalBluetoothManager} or null if Bluetooth is not
* supported for this hardware. This instance should be globally cached by the caller.
*/
@Nullable
public static LocalBluetoothManager create(Context context, Handler handler) {
LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance();
if (adapter == null) {
return null;
}
return new LocalBluetoothManager(adapter, context, handler, /* userHandle= */ null);
}
/**
* Returns a new instance of {@link LocalBluetoothManager} or null if Bluetooth is not
* supported for this hardware. This instance should be globally cached by the caller.
*
* <p> Allows to specify a {@link UserHandle} for which to receive bluetooth events.
*
* <p> Requires {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission.
*/
@Nullable
@RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
public static LocalBluetoothManager create(Context context, Handler handler,
UserHandle userHandle) {
LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance();
if (adapter == null) {
return null;
}
return new LocalBluetoothManager(adapter, context, handler,
userHandle);
}
private LocalBluetoothManager(LocalBluetoothAdapter adapter, Context context, Handler handler,
UserHandle userHandle) {
mContext = context.getApplicationContext();
mLocalAdapter = adapter;
mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, this);
mEventManager = new BluetoothEventManager(mLocalAdapter, mCachedDeviceManager, mContext,
handler, userHandle);
mProfileManager = new LocalBluetoothProfileManager(mContext,
mLocalAdapter, mCachedDeviceManager, mEventManager);
mProfileManager.updateLocalProfiles();
mEventManager.readPairedDevices();
}
public LocalBluetoothAdapter getBluetoothAdapter() {
return mLocalAdapter;
}
public Context getContext() {
return mContext;
}
public Context getForegroundActivity() {
return mForegroundActivity == null
? null
: mForegroundActivity.get();
}
public boolean isForegroundActivity() {
return mForegroundActivity != null && mForegroundActivity.get() != null;
}
public synchronized void setForegroundActivity(Context context) {
if (context != null) {
Log.d(TAG, "setting foreground activity to non-null context");
mForegroundActivity = new WeakReference<>(context);
} else {
if (mForegroundActivity != null) {
Log.d(TAG, "setting foreground activity to null");
mForegroundActivity = null;
}
}
}
public CachedBluetoothDeviceManager getCachedDeviceManager() {
return mCachedDeviceManager;
}
public BluetoothEventManager getEventManager() {
return mEventManager;
}
public LocalBluetoothProfileManager getProfileManager() {
return mProfileManager;
}
public interface BluetoothManagerCallback {
void onBluetoothManagerInitialized(Context appContext,
LocalBluetoothManager bluetoothManager);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 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.settingslib.bluetooth
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
/** Returns a [Flow] that emits a [Unit] whenever the headset audio mode changes. */
val LocalBluetoothManager.headsetAudioModeChanges: Flow<Unit>
get() {
return callbackFlow {
val callback =
object : BluetoothCallback {
override fun onAudioModeChanged() {
launch { send(Unit) }
}
}
eventManager.registerCallback(callback)
awaitClose { eventManager.unregisterCallback(callback) }
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
/**
* LocalBluetoothProfile is an interface defining the basic
* functionality related to a Bluetooth profile.
*/
public interface LocalBluetoothProfile {
/**
* Return {@code true} if the user can initiate a connection for this profile in UI.
*/
boolean accessProfileEnabled();
/**
* Returns true if the user can enable auto connection for this profile.
*/
boolean isAutoConnectable();
int getConnectionStatus(BluetoothDevice device);
/**
* Return {@code true} if the profile is enabled, otherwise return {@code false}.
* @param device the device to query for enable status
*/
boolean isEnabled(BluetoothDevice device);
/**
* Get the connection policy of the profile.
* @param device the device to query for enable status
*/
int getConnectionPolicy(BluetoothDevice device);
/**
* Enable the profile if {@code enabled} is {@code true}, otherwise disable profile.
* @param device the device to set profile status
* @param enabled {@code true} for enable profile, otherwise disable profile.
*/
boolean setEnabled(BluetoothDevice device, boolean enabled);
boolean isProfileReady();
int getProfileId();
/** Display order for device profile settings. */
int getOrdinal();
/**
* Returns the string resource ID for the localized name for this profile.
* @param device the Bluetooth device (to distinguish between PAN roles)
*/
int getNameResource(BluetoothDevice device);
/**
* Returns the string resource ID for the summary text for this profile
* for the specified device, e.g. "Use for media audio" or
* "Connected to media audio".
* @param device the device to query for profile connection status
* @return a string resource ID for the profile summary text
*/
int getSummaryResourceForDevice(BluetoothDevice device);
int getDrawableResource(BluetoothClass btClass);
}

View File

@@ -0,0 +1,737 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothPan;
import android.bluetooth.BluetoothPbap;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSap;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* LocalBluetoothProfileManager provides access to the LocalBluetoothProfile
* objects for the available Bluetooth profiles.
*/
public class LocalBluetoothProfileManager {
private static final String TAG = "LocalBluetoothProfileManager";
private static final boolean DEBUG = BluetoothUtils.D;
/**
* An interface for notifying BluetoothHeadset IPC clients when they have
* been connected to the BluetoothHeadset service.
* Only used by com.android.settings.bluetooth.DockService.
*/
public interface ServiceListener {
/**
* Called to notify the client when this proxy object has been
* connected to the BluetoothHeadset service. Clients must wait for
* this callback before making IPC calls on the BluetoothHeadset
* service.
*/
void onServiceConnected();
/**
* Called to notify the client that this proxy object has been
* disconnected from the BluetoothHeadset service. Clients must not
* make IPC calls on the BluetoothHeadset service after this callback.
* This callback will currently only occur if the application hosting
* the BluetoothHeadset service, but may be called more often in future.
*/
void onServiceDisconnected();
}
private final Context mContext;
private final CachedBluetoothDeviceManager mDeviceManager;
private final BluetoothEventManager mEventManager;
private A2dpProfile mA2dpProfile;
private A2dpSinkProfile mA2dpSinkProfile;
private HeadsetProfile mHeadsetProfile;
private HfpClientProfile mHfpClientProfile;
private MapProfile mMapProfile;
private MapClientProfile mMapClientProfile;
private HidProfile mHidProfile;
private HidDeviceProfile mHidDeviceProfile;
private OppProfile mOppProfile;
private PanProfile mPanProfile;
private PbapClientProfile mPbapClientProfile;
private PbapServerProfile mPbapProfile;
private HearingAidProfile mHearingAidProfile;
private HapClientProfile mHapClientProfile;
private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
private LeAudioProfile mLeAudioProfile;
private LocalBluetoothLeBroadcast mLeAudioBroadcast;
private LocalBluetoothLeBroadcastAssistant mLeAudioBroadcastAssistant;
private SapProfile mSapProfile;
private VolumeControlProfile mVolumeControlProfile;
/**
* Mapping from profile name, e.g. "HEADSET" to profile object.
*/
private final Map<String, LocalBluetoothProfile>
mProfileNameMap = new HashMap<String, LocalBluetoothProfile>();
LocalBluetoothProfileManager(Context context,
LocalBluetoothAdapter adapter,
CachedBluetoothDeviceManager deviceManager,
BluetoothEventManager eventManager) {
mContext = context;
mDeviceManager = deviceManager;
mEventManager = eventManager;
// pass this reference to adapter and event manager (circular dependency)
adapter.setProfileManager(this);
if (DEBUG) Log.d(TAG, "LocalBluetoothProfileManager construction complete");
}
/**
* create profile instance according to bluetooth supported profile list
*/
synchronized void updateLocalProfiles() {
List<Integer> supportedList = BluetoothAdapter.getDefaultAdapter().getSupportedProfiles();
if (CollectionUtils.isEmpty(supportedList)) {
if (DEBUG) Log.d(TAG, "supportedList is null");
return;
}
if (mA2dpProfile == null && supportedList.contains(BluetoothProfile.A2DP)) {
if (DEBUG) Log.d(TAG, "Adding local A2DP profile");
mA2dpProfile = new A2dpProfile(mContext, mDeviceManager, this);
addProfile(mA2dpProfile, A2dpProfile.NAME,
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
}
if (mA2dpSinkProfile == null && supportedList.contains(BluetoothProfile.A2DP_SINK)) {
if (DEBUG) Log.d(TAG, "Adding local A2DP SINK profile");
mA2dpSinkProfile = new A2dpSinkProfile(mContext, mDeviceManager, this);
addProfile(mA2dpSinkProfile, A2dpSinkProfile.NAME,
BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
}
if (mHeadsetProfile == null && supportedList.contains(BluetoothProfile.HEADSET)) {
if (DEBUG) Log.d(TAG, "Adding local HEADSET profile");
mHeadsetProfile = new HeadsetProfile(mContext, mDeviceManager, this);
addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME,
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
}
if (mHfpClientProfile == null && supportedList.contains(BluetoothProfile.HEADSET_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local HfpClient profile");
mHfpClientProfile = new HfpClientProfile(mContext, mDeviceManager, this);
addProfile(mHfpClientProfile, HfpClientProfile.NAME,
BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
}
if (mMapClientProfile == null && supportedList.contains(BluetoothProfile.MAP_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local MAP CLIENT profile");
mMapClientProfile = new MapClientProfile(mContext, mDeviceManager,this);
addProfile(mMapClientProfile, MapClientProfile.NAME,
BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
}
if (mMapProfile == null && supportedList.contains(BluetoothProfile.MAP)) {
if (DEBUG) Log.d(TAG, "Adding local MAP profile");
mMapProfile = new MapProfile(mContext, mDeviceManager, this);
addProfile(mMapProfile, MapProfile.NAME, BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
}
if (mOppProfile == null && supportedList.contains(BluetoothProfile.OPP)) {
if (DEBUG) Log.d(TAG, "Adding local OPP profile");
mOppProfile = new OppProfile();
// Note: no event handler for OPP, only name map.
mProfileNameMap.put(OppProfile.NAME, mOppProfile);
}
if (mHearingAidProfile == null && supportedList.contains(BluetoothProfile.HEARING_AID)) {
if (DEBUG) Log.d(TAG, "Adding local Hearing Aid profile");
mHearingAidProfile = new HearingAidProfile(mContext, mDeviceManager,
this);
addProfile(mHearingAidProfile, HearingAidProfile.NAME,
BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
}
if (mHapClientProfile == null && supportedList.contains(BluetoothProfile.HAP_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local HAP_CLIENT profile");
mHapClientProfile = new HapClientProfile(mContext, mDeviceManager, this);
addProfile(mHapClientProfile, HapClientProfile.NAME,
BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
}
if (mHidProfile == null && supportedList.contains(BluetoothProfile.HID_HOST)) {
if (DEBUG) Log.d(TAG, "Adding local HID_HOST profile");
mHidProfile = new HidProfile(mContext, mDeviceManager, this);
addProfile(mHidProfile, HidProfile.NAME,
BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
}
if (mHidDeviceProfile == null && supportedList.contains(BluetoothProfile.HID_DEVICE)) {
if (DEBUG) Log.d(TAG, "Adding local HID_DEVICE profile");
mHidDeviceProfile = new HidDeviceProfile(mContext, mDeviceManager, this);
addProfile(mHidDeviceProfile, HidDeviceProfile.NAME,
BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
}
if (mPanProfile == null && supportedList.contains(BluetoothProfile.PAN)) {
if (DEBUG) Log.d(TAG, "Adding local PAN profile");
mPanProfile = new PanProfile(mContext);
addPanProfile(mPanProfile, PanProfile.NAME,
BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
}
if (mPbapProfile == null && supportedList.contains(BluetoothProfile.PBAP)) {
if (DEBUG) Log.d(TAG, "Adding local PBAP profile");
mPbapProfile = new PbapServerProfile(mContext);
addProfile(mPbapProfile, PbapServerProfile.NAME,
BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED);
}
if (mPbapClientProfile == null && supportedList.contains(BluetoothProfile.PBAP_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local PBAP Client profile");
mPbapClientProfile = new PbapClientProfile(mContext, mDeviceManager,this);
addProfile(mPbapClientProfile, PbapClientProfile.NAME,
BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
}
if (mSapProfile == null && supportedList.contains(BluetoothProfile.SAP)) {
if (DEBUG) {
Log.d(TAG, "Adding local SAP profile");
}
mSapProfile = new SapProfile(mContext, mDeviceManager, this);
addProfile(mSapProfile, SapProfile.NAME, BluetoothSap.ACTION_CONNECTION_STATE_CHANGED);
}
if (mVolumeControlProfile == null
&& supportedList.contains(BluetoothProfile.VOLUME_CONTROL)) {
if (DEBUG) {
Log.d(TAG, "Adding local Volume Control profile");
}
mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this);
addProfile(mVolumeControlProfile, VolumeControlProfile.NAME,
BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED);
}
if (mLeAudioProfile == null && supportedList.contains(BluetoothProfile.LE_AUDIO)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO profile");
}
mLeAudioProfile = new LeAudioProfile(mContext, mDeviceManager, this);
addProfile(mLeAudioProfile, LeAudioProfile.NAME,
BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
}
if (mLeAudioBroadcast == null
&& supportedList.contains(BluetoothProfile.LE_AUDIO_BROADCAST)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO_BROADCAST profile");
}
mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager);
// no event handler for the LE boradcast.
mProfileNameMap.put(LocalBluetoothLeBroadcast.NAME, mLeAudioBroadcast);
}
if (mLeAudioBroadcastAssistant == null
&& supportedList.contains(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO_BROADCAST_ASSISTANT profile");
}
mLeAudioBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(mContext,
mDeviceManager, this);
addProfile(mLeAudioBroadcastAssistant, LocalBluetoothLeBroadcast.NAME,
BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED);
}
if (mCsipSetCoordinatorProfile == null
&& supportedList.contains(BluetoothProfile.CSIP_SET_COORDINATOR)) {
if (DEBUG) {
Log.d(TAG, "Adding local CSIP set coordinator profile");
}
mCsipSetCoordinatorProfile =
new CsipSetCoordinatorProfile(mContext, mDeviceManager, this);
addProfile(mCsipSetCoordinatorProfile, mCsipSetCoordinatorProfile.NAME,
BluetoothCsipSetCoordinator.ACTION_CSIS_CONNECTION_STATE_CHANGED);
}
mEventManager.registerProfileIntentReceiver();
}
private void addHeadsetProfile(LocalBluetoothProfile profile, String profileName,
String stateChangedAction, String audioStateChangedAction, int audioDisconnectedState) {
BluetoothEventManager.Handler handler = new HeadsetStateChangeHandler(
profile, audioStateChangedAction, audioDisconnectedState);
mEventManager.addProfileHandler(stateChangedAction, handler);
mEventManager.addProfileHandler(audioStateChangedAction, handler);
mProfileNameMap.put(profileName, profile);
}
private final Collection<ServiceListener> mServiceListeners =
new CopyOnWriteArrayList<ServiceListener>();
private void addProfile(LocalBluetoothProfile profile,
String profileName, String stateChangedAction) {
mEventManager.addProfileHandler(stateChangedAction, new StateChangedHandler(profile));
mProfileNameMap.put(profileName, profile);
}
private void addPanProfile(LocalBluetoothProfile profile,
String profileName, String stateChangedAction) {
mEventManager.addProfileHandler(stateChangedAction,
new PanStateChangedHandler(profile));
mProfileNameMap.put(profileName, profile);
}
public LocalBluetoothProfile getProfileByName(String name) {
return mProfileNameMap.get(name);
}
// Called from LocalBluetoothAdapter when state changes to ON
void setBluetoothStateOn() {
updateLocalProfiles();
mEventManager.readPairedDevices();
}
/**
* Generic handler for connection state change events for the specified profile.
*/
private class StateChangedHandler implements BluetoothEventManager.Handler {
final LocalBluetoothProfile mProfile;
StateChangedHandler(LocalBluetoothProfile profile) {
mProfile = profile;
}
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
Log.w(TAG, "StateChangedHandler found new device: " + device);
cachedDevice = mDeviceManager.addDevice(device);
}
onReceiveInternal(intent, cachedDevice);
}
protected void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
if (newState == BluetoothProfile.STATE_DISCONNECTED &&
oldState == BluetoothProfile.STATE_CONNECTING) {
Log.i(TAG, "Failed to connect " + mProfile + " device");
}
if (getHearingAidProfile() != null
&& mProfile instanceof HearingAidProfile
&& (newState == BluetoothProfile.STATE_CONNECTED)) {
// Check if the HiSyncID has being initialized
if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice());
if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
final BluetoothDevice device = cachedDevice.getDevice();
final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
.setAshaDeviceSide(getHearingAidProfile().getDeviceSide(device))
.setAshaDeviceMode(getHearingAidProfile().getDeviceMode(device))
.setHiSyncId(newHiSyncId);
cachedDevice.setHearingAidInfo(infoBuilder.build());
}
}
HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
}
final boolean isHapClientProfile = getHapClientProfile() != null
&& mProfile instanceof HapClientProfile;
final boolean isLeAudioProfile = getLeAudioProfile() != null
&& mProfile instanceof LeAudioProfile;
final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile;
if (isHapClientOrLeAudioProfile && newState == BluetoothProfile.STATE_CONNECTED) {
// Checks if both profiles are connected to the device. Hearing aid info need
// to be retrieved from these profiles separately.
if (cachedDevice.isConnectedLeAudioHearingAidDevice()) {
final BluetoothDevice device = cachedDevice.getDevice();
final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
.setLeAudioLocation(getLeAudioProfile().getAudioLocation(device))
.setHapDeviceType(getHapClientProfile().getHearingAidType(device));
cachedDevice.setHearingAidInfo(infoBuilder.build());
HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
}
}
if (getCsipSetCoordinatorProfile() != null
&& mProfile instanceof CsipSetCoordinatorProfile
&& newState == BluetoothProfile.STATE_CONNECTED) {
// Check if the GroupID has being initialized
if (cachedDevice.getGroupId() == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
final Map<Integer, ParcelUuid> groupIdMap = getCsipSetCoordinatorProfile()
.getGroupUuidMapByDevice(cachedDevice.getDevice());
if (groupIdMap != null) {
for (Map.Entry<Integer, ParcelUuid> entry: groupIdMap.entrySet()) {
if (entry.getValue().equals(BluetoothUuid.CAP)) {
cachedDevice.setGroupId(entry.getKey());
break;
}
}
}
}
}
cachedDevice.onProfileStateChanged(mProfile, newState);
// Dispatch profile changed after device update
boolean needDispatchProfileConnectionState = true;
if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
|| cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
needDispatchProfileConnectionState = !mDeviceManager
.onProfileConnectionStateChangedIfProcessed(cachedDevice, newState,
mProfile.getProfileId());
}
if (needDispatchProfileConnectionState) {
cachedDevice.refresh();
mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, newState,
mProfile.getProfileId());
}
}
}
/** Connectivity and audio state change handler for headset profiles. */
private class HeadsetStateChangeHandler extends StateChangedHandler {
private final String mAudioChangeAction;
private final int mAudioDisconnectedState;
HeadsetStateChangeHandler(LocalBluetoothProfile profile, String audioChangeAction,
int audioDisconnectedState) {
super(profile);
mAudioChangeAction = audioChangeAction;
mAudioDisconnectedState = audioDisconnectedState;
}
@Override
public void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
if (mAudioChangeAction.equals(intent.getAction())) {
int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
if (newState != mAudioDisconnectedState) {
cachedDevice.onProfileStateChanged(mProfile, BluetoothProfile.STATE_CONNECTED);
}
cachedDevice.refresh();
} else {
super.onReceiveInternal(intent, cachedDevice);
}
}
}
/** State change handler for NAP and PANU profiles. */
private class PanStateChangedHandler extends StateChangedHandler {
PanStateChangedHandler(LocalBluetoothProfile profile) {
super(profile);
}
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
PanProfile panProfile = (PanProfile) mProfile;
int role = intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, 0);
panProfile.setLocalRole(device, role);
super.onReceive(context, intent, device);
}
}
// called from DockService
public void addServiceListener(ServiceListener l) {
mServiceListeners.add(l);
}
// called from DockService
public void removeServiceListener(ServiceListener l) {
mServiceListeners.remove(l);
}
// not synchronized: use only from UI thread! (TODO: verify)
void callServiceConnectedListeners() {
final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners);
for (ServiceListener l : listeners) {
l.onServiceConnected();
}
}
// not synchronized: use only from UI thread! (TODO: verify)
void callServiceDisconnectedListeners() {
final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners);
for (ServiceListener listener : listeners) {
listener.onServiceDisconnected();
}
}
// This is called by DockService, so check Headset and A2DP.
public synchronized boolean isManagerReady() {
// Getting just the headset profile is fine for now. Will need to deal with A2DP
// and others if they aren't always in a ready state.
LocalBluetoothProfile profile = mHeadsetProfile;
if (profile != null) {
return profile.isProfileReady();
}
profile = mA2dpProfile;
if (profile != null) {
return profile.isProfileReady();
}
profile = mA2dpSinkProfile;
if (profile != null) {
return profile.isProfileReady();
}
return false;
}
public A2dpProfile getA2dpProfile() {
return mA2dpProfile;
}
public A2dpSinkProfile getA2dpSinkProfile() {
if ((mA2dpSinkProfile != null) && (mA2dpSinkProfile.isProfileReady())) {
return mA2dpSinkProfile;
} else {
return null;
}
}
public HeadsetProfile getHeadsetProfile() {
return mHeadsetProfile;
}
public HfpClientProfile getHfpClientProfile() {
if ((mHfpClientProfile != null) && (mHfpClientProfile.isProfileReady())) {
return mHfpClientProfile;
} else {
return null;
}
}
public PbapClientProfile getPbapClientProfile() {
return mPbapClientProfile;
}
public PbapServerProfile getPbapProfile(){
return mPbapProfile;
}
public MapProfile getMapProfile(){
return mMapProfile;
}
public MapClientProfile getMapClientProfile() {
return mMapClientProfile;
}
public HearingAidProfile getHearingAidProfile() {
return mHearingAidProfile;
}
public HapClientProfile getHapClientProfile() {
return mHapClientProfile;
}
public LeAudioProfile getLeAudioProfile() {
return mLeAudioProfile;
}
public LocalBluetoothLeBroadcast getLeAudioBroadcastProfile() {
return mLeAudioBroadcast;
}
public LocalBluetoothLeBroadcastAssistant getLeAudioBroadcastAssistantProfile() {
return mLeAudioBroadcastAssistant;
}
SapProfile getSapProfile() {
return mSapProfile;
}
@VisibleForTesting
HidProfile getHidProfile() {
return mHidProfile;
}
@VisibleForTesting
HidDeviceProfile getHidDeviceProfile() {
return mHidDeviceProfile;
}
public CsipSetCoordinatorProfile getCsipSetCoordinatorProfile() {
return mCsipSetCoordinatorProfile;
}
public VolumeControlProfile getVolumeControlProfile() {
return mVolumeControlProfile;
}
/**
* Fill in a list of LocalBluetoothProfile objects that are supported by
* the local device and the remote device.
*
* @param uuids of the remote device
* @param localUuids UUIDs of the local device
* @param profiles The list of profiles to fill
* @param removedProfiles list of profiles that were removed
*/
synchronized void updateProfiles(ParcelUuid[] uuids, ParcelUuid[] localUuids,
Collection<LocalBluetoothProfile> profiles,
Collection<LocalBluetoothProfile> removedProfiles,
boolean isPanNapConnected, BluetoothDevice device) {
// Copy previous profile list into removedProfiles
removedProfiles.clear();
removedProfiles.addAll(profiles);
if (DEBUG) {
Log.d(TAG,"Current Profiles" + profiles.toString());
}
profiles.clear();
if (uuids == null) {
return;
}
// The profiles list's sequence will affect the bluetooth icon at
// BluetoothUtils.getBtClassDrawableWithDescription(Context,CachedBluetoothDevice).
// Moving the LE audio profile to be the first priority if the device supports LE audio.
if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
profiles.add(mLeAudioProfile);
removedProfiles.remove(mLeAudioProfile);
}
if (mHeadsetProfile != null) {
if ((ArrayUtils.contains(localUuids, BluetoothUuid.HSP_AG)
&& ArrayUtils.contains(uuids, BluetoothUuid.HSP))
|| (ArrayUtils.contains(localUuids, BluetoothUuid.HFP_AG)
&& ArrayUtils.contains(uuids, BluetoothUuid.HFP))) {
profiles.add(mHeadsetProfile);
removedProfiles.remove(mHeadsetProfile);
}
}
if ((mHfpClientProfile != null) &&
ArrayUtils.contains(uuids, BluetoothUuid.HFP_AG)
&& ArrayUtils.contains(localUuids, BluetoothUuid.HFP)) {
profiles.add(mHfpClientProfile);
removedProfiles.remove(mHfpClientProfile);
}
if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS) && mA2dpProfile != null) {
profiles.add(mA2dpProfile);
removedProfiles.remove(mA2dpProfile);
}
if (BluetoothUuid.containsAnyUuid(uuids, A2dpSinkProfile.SRC_UUIDS)
&& mA2dpSinkProfile != null) {
profiles.add(mA2dpSinkProfile);
removedProfiles.remove(mA2dpSinkProfile);
}
if (ArrayUtils.contains(uuids, BluetoothUuid.OBEX_OBJECT_PUSH) && mOppProfile != null) {
profiles.add(mOppProfile);
removedProfiles.remove(mOppProfile);
}
if ((ArrayUtils.contains(uuids, BluetoothUuid.HID)
|| ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) && mHidProfile != null) {
profiles.add(mHidProfile);
removedProfiles.remove(mHidProfile);
}
if (mHidDeviceProfile != null && mHidDeviceProfile.getConnectionStatus(device)
!= BluetoothProfile.STATE_DISCONNECTED) {
profiles.add(mHidDeviceProfile);
removedProfiles.remove(mHidDeviceProfile);
}
if(isPanNapConnected)
if(DEBUG) Log.d(TAG, "Valid PAN-NAP connection exists.");
if ((ArrayUtils.contains(uuids, BluetoothUuid.NAP) && mPanProfile != null)
|| isPanNapConnected) {
profiles.add(mPanProfile);
removedProfiles.remove(mPanProfile);
}
if ((mMapProfile != null) &&
(mMapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
profiles.add(mMapProfile);
removedProfiles.remove(mMapProfile);
mMapProfile.setEnabled(device, true);
}
if ((mPbapProfile != null) &&
(mPbapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
profiles.add(mPbapProfile);
removedProfiles.remove(mPbapProfile);
mPbapProfile.setEnabled(device, true);
}
if ((mMapClientProfile != null)
&& BluetoothUuid.containsAnyUuid(uuids, MapClientProfile.UUIDS)) {
profiles.add(mMapClientProfile);
removedProfiles.remove(mMapClientProfile);
}
if ((mPbapClientProfile != null)
&& BluetoothUuid.containsAnyUuid(uuids, PbapClientProfile.SRC_UUIDS)) {
profiles.add(mPbapClientProfile);
removedProfiles.remove(mPbapClientProfile);
}
if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID) && mHearingAidProfile != null) {
profiles.add(mHearingAidProfile);
removedProfiles.remove(mHearingAidProfile);
}
if (mHapClientProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.HAS)) {
profiles.add(mHapClientProfile);
removedProfiles.remove(mHapClientProfile);
}
if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
profiles.add(mSapProfile);
removedProfiles.remove(mSapProfile);
}
if (mVolumeControlProfile != null
&& ArrayUtils.contains(uuids, BluetoothUuid.VOLUME_CONTROL)) {
profiles.add(mVolumeControlProfile);
removedProfiles.remove(mVolumeControlProfile);
}
if (mCsipSetCoordinatorProfile != null
&& ArrayUtils.contains(uuids, BluetoothUuid.COORDINATED_SET)) {
profiles.add(mCsipSetCoordinatorProfile);
removedProfiles.remove(mCsipSetCoordinatorProfile);
}
if (DEBUG) {
Log.d(TAG,"New Profiles" + profiles.toString());
}
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* MapClientProfile handles the Bluetooth MAP MCE role.
*/
public final class MapClientProfile implements LocalBluetoothProfile {
private static final String TAG = "MapClientProfile";
private BluetoothMapClient mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.MAS,
};
static final String NAME = "MAP Client";
// Order of this profile in device profiles list
private static final int ORDINAL = 0;
// These callbacks run on the main thread.
private final class MapClientServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothMapClient) proxy;
// We just bound to the service, so refresh the UI for any connected MAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "MapProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(MapClientProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
Log.d(TAG, "isProfileReady(): "+ mIsProfileReady);
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.MAP_CLIENT;
}
MapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new MapClientServiceListener(), BluetoothProfile.MAP_CLIENT);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_map;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_map_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_map_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.MAP_CLIENT,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up MAP Client proxy", t);
}
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* MapProfile handles the Bluetooth MAP MSE role
*/
public class MapProfile implements LocalBluetoothProfile {
private static final String TAG = "MapProfile";
private BluetoothMap mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.MAP,
BluetoothUuid.MNS,
BluetoothUuid.MAS,
};
static final String NAME = "MAP";
// Order of this profile in device profiles list
// These callbacks run on the main thread.
private final class MapServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothMap) proxy;
// We just bound to the service, so refresh the UI for any connected MAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "MapProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(MapProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
Log.d(TAG, "isProfileReady(): " + mIsProfileReady);
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.MAP;
}
MapProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new MapServiceListener(),
BluetoothProfile.MAP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return BluetoothProfile.MAP;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_map;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_map_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_map_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.MAP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up MAP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,12 @@
# Default reviewers for this and subdirectories.
siyuanh@google.com
hughchen@google.com
timhypeng@google.com
robertluo@google.com
songferngwang@google.com
yqian@google.com
chelseahao@google.com
yiyishen@google.com
hahong@google.com
# Emergency approvers in case the above are not available

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import com.android.settingslib.R;
/**
* OppProfile handles Bluetooth OPP.
*/
final class OppProfile implements LocalBluetoothProfile {
static final String NAME = "OPP";
// Order of this profile in device profiles list
private static final int ORDINAL = 2;
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return false;
}
public int getConnectionStatus(BluetoothDevice device) {
return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle OPP
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return false;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle OPP
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
return false;
}
public boolean isProfileReady() {
return true;
}
@Override
public int getProfileId() {
return BluetoothProfile.OPP;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_opp;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
return 0; // OPP profile not displayed in UI
}
public int getDrawableResource(BluetoothClass btClass) {
return 0; // no icon for OPP
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothPan;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.R;
import java.util.HashMap;
import java.util.List;
/**
* PanProfile handles Bluetooth PAN profile (NAP and PANU).
*/
public class PanProfile implements LocalBluetoothProfile {
private static final String TAG = "PanProfile";
private BluetoothPan mService;
private boolean mIsProfileReady;
// Tethering direction for each device
private final HashMap<BluetoothDevice, Integer> mDeviceRoleMap =
new HashMap<BluetoothDevice, Integer>();
static final String NAME = "PAN";
// Order of this profile in device profiles list
private static final int ORDINAL = 4;
// These callbacks run on the main thread.
private final class PanServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothPan) proxy;
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.PAN;
}
PanProfile(Context context) {
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new PanServiceListener(),
BluetoothProfile.PAN);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return false;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return true;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return -1;
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
final List<BluetoothDevice> sinks = mService.getConnectedDevices();
if (sinks != null) {
for (BluetoothDevice sink : sinks) {
mService.setConnectionPolicy(sink, CONNECTION_POLICY_FORBIDDEN);
}
}
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
if (isLocalRoleNap(device)) {
return R.string.bluetooth_profile_pan_nap;
} else {
return R.string.bluetooth_profile_pan;
}
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_pan_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
if (isLocalRoleNap(device)) {
return R.string.bluetooth_pan_nap_profile_summary_connected;
} else {
return R.string.bluetooth_pan_user_profile_summary_connected;
}
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_network_pan;
}
// Tethering direction determines UI strings.
void setLocalRole(BluetoothDevice device, int role) {
mDeviceRoleMap.put(device, role);
}
boolean isLocalRoleNap(BluetoothDevice device) {
if (mDeviceRoleMap.containsKey(device)) {
return mDeviceRoleMap.get(device) == BluetoothPan.LOCAL_NAP_ROLE;
} else {
return false;
}
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.PAN, mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up PAN proxy", t);
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2016 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class PbapClientProfile implements LocalBluetoothProfile {
private static final String TAG = "PbapClientProfile";
private BluetoothPbapClient mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final ParcelUuid[] SRC_UUIDS = {
BluetoothUuid.PBAP_PSE,
};
static final String NAME = "PbapClient";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 6;
// These callbacks run on the main thread.
private final class PbapClientServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothPbapClient) proxy;
// We just bound to the service, so refresh the UI for any connected PBAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "PbapClientProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(PbapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
}
}
private void refreshProfiles() {
Collection<CachedBluetoothDevice> cachedDevices = mDeviceManager.getCachedDevicesCopy();
for (CachedBluetoothDevice device : cachedDevices) {
device.onUuidChanged();
}
}
public boolean pbapClientExists() {
return (mService != null);
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.PBAP_CLIENT;
}
PbapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new PbapClientServiceListener(), BluetoothProfile.PBAP_CLIENT);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
// we need to have same string in UI as the server side.
return R.string.bluetooth_profile_pbap;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
return R.string.bluetooth_profile_pbap_summary;
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.PBAP_CLIENT,mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up PBAP Client proxy", t);
}
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2011 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothPbap;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.R;
/**
* PBAPServer Profile
*/
public class PbapServerProfile implements LocalBluetoothProfile {
private static final String TAG = "PbapServerProfile";
private BluetoothPbap mService;
private boolean mIsProfileReady;
@VisibleForTesting
public static final String NAME = "PBAP Server";
// Order of this profile in device profiles list
private static final int ORDINAL = 6;
// The UUIDs indicate that remote device might access pbap server
static final ParcelUuid[] PBAB_CLIENT_UUIDS = {
BluetoothUuid.HSP,
BluetoothUuid.HFP,
BluetoothUuid.PBAP_PCE
};
// These callbacks run on the main thread.
private final class PbapServiceListener
implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothPbap) proxy;
mIsProfileReady=true;
}
@Override
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.PBAP;
}
PbapServerProfile(Context context) {
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new PbapServiceListener(),
BluetoothProfile.PBAP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return false;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) return BluetoothProfile.STATE_DISCONNECTED;
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return false;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return -1;
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (!enabled) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_pbap;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
return R.string.bluetooth_profile_pbap_summary;
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.PBAP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up PBAP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2015 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSap;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* SapProfile handles Bluetooth SAP profile.
*/
final class SapProfile implements LocalBluetoothProfile {
private static final String TAG = "SapProfile";
private BluetoothSap mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.SAP,
};
static final String NAME = "SAP";
// Order of this profile in device profiles list
private static final int ORDINAL = 10;
// These callbacks run on the main thread.
private final class SapServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothSap) proxy;
// We just bound to the service, so refresh the UI for any connected SAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "SapProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(SapProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.SAP;
}
SapProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new SapServiceListener(),
BluetoothProfile.SAP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_sap;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_sap_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_sap_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.SAP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up SAP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright (C) 2021 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/** VolumeControlProfile handles Bluetooth Volume Control Controller role */
public class VolumeControlProfile implements LocalBluetoothProfile {
private static final String TAG = "VolumeControlProfile";
private static boolean DEBUG = true;
static final String NAME = "VCP";
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private Context mContext;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
private BluetoothVolumeControl mService;
private boolean mIsProfileReady;
// These callbacks run on the main thread.
private final class VolumeControlProfileServiceListener
implements BluetoothProfile.ServiceListener {
@RequiresApi(Build.VERSION_CODES.S)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothVolumeControl) proxy;
// We just bound to the service, so refresh the UI for any connected
// VolumeControlProfile devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
VolumeControlProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
VolumeControlProfile(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(
context,
new VolumeControlProfile.VolumeControlProfileServiceListener(),
BluetoothProfile.VOLUME_CONTROL);
}
/**
* Registers a {@link BluetoothVolumeControl.Callback} that will be invoked during the operation
* of this profile.
*
* <p>Repeated registration of the same <var>callback</var> object will have no effect after the
* first call to this method, even when the <var>executor</var> is different. API caller would
* have to call {@link #unregisterCallback(BluetoothVolumeControl.Callback)} with the same
* callback object before registering it again.
*
* @param executor an {@link Executor} to execute given callback
* @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
* @throws IllegalArgumentException if a null executor or callback is given
*/
public void registerCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothVolumeControl.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
return;
}
mService.registerCallback(executor, callback);
}
/**
* Unregisters the specified {@link BluetoothVolumeControl.Callback}.
*
* <p>The same {@link BluetoothVolumeControl.Callback} object used when calling {@link
* #registerCallback(Executor, BluetoothVolumeControl.Callback)} must be used.
*
* <p>Callbacks are automatically unregistered when application process goes away
*
* @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
* @throws IllegalArgumentException when callback is null or when no callback is registered
*/
public void unregisterCallback(@NonNull BluetoothVolumeControl.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
return;
}
mService.unregisterCallback(callback);
}
/**
* Tells the remote device to set a volume offset to the absolute volume.
*
* @param device {@link BluetoothDevice} representing the remote device
* @param volumeOffset volume offset to be set on the remote device
*/
public void setVolumeOffset(
BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
return;
}
if (device == null) {
Log.w(TAG, "Device is null. Cannot set volume offset.");
return;
}
mService.setVolumeOffset(device, volumeOffset);
}
/**
* Provides information about the possibility to set volume offset on the remote device. If the
* remote device supports Volume Offset Control Service, it is automatically connected.
*
* @param device {@link BluetoothDevice} representing the remote device
* @return {@code true} if volume offset function is supported and available to use on the
* remote device. When Bluetooth is off, the return value should always be {@code false}.
*/
public boolean isVolumeOffsetAvailable(BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get is volume offset available.");
return false;
}
if (device == null) {
Log.w(TAG, "Device is null. Cannot get is volume offset available.");
return false;
}
return mService.isVolumeOffsetAvailable(device);
}
/**
* Tells the remote device to set a volume.
*
* @param device {@link BluetoothDevice} representing the remote device
* @param volume volume to be set on the remote device
* @param isGroupOp whether to set the volume to remote devices within the same CSIP group
*/
public void setDeviceVolume(
BluetoothDevice device,
@IntRange(from = 0, to = 255) int volume,
boolean isGroupOp) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
return;
}
if (device == null) {
Log.w(TAG, "Device is null. Cannot set volume offset.");
return;
}
mService.setDeviceVolume(device, volume, isGroupOp);
}
@Override
public boolean accessProfileEnabled() {
return false;
}
@Override
public boolean isAutoConnectable() {
return true;
}
/**
* Gets VolumeControlProfile devices matching connection states{ {@code
* BluetoothProfile.STATE_CONNECTED}, {@code BluetoothProfile.STATE_CONNECTING}, {@code
* BluetoothProfile.STATE_DISCONNECTING}}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (DEBUG) {
Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled);
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.VOLUME_CONTROL;
}
public String toString() {
return NAME;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return 0; // VCP profile not displayed in UI
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
return 0; // VCP profile not displayed in UI
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
// no icon for VCP
return 0;
}
}