fix: 首次提交
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;";
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, "");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
Normal file
12
SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user