Files
app_Settings/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
Yiyi Shen 2fc788c35a [Audiosharing] Enable audio sharing UI when preview option on.
Test: atest
Bug: 368401233
Flag: com.android.settingslib.flags.audio_sharing_developer_option
Change-Id: Idbc84e2c43f7361c58c440d1a7d7c78edd3c0521
2024-11-04 14:27:42 +08:00

413 lines
17 KiB
Java

/*
* 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.settings.bluetooth;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.accessibility.AccessibilityStatsLogUtils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingIncompatibleDialogFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Abstract class for providing basic interaction for a list of Bluetooth devices in bluetooth
* device pairing detail page.
*/
public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment {
private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10);
private static final int AUTO_DISMISS_MESSAGE_ID = 1001;
protected boolean mInitialScanStarted;
@VisibleForTesting
protected BluetoothProgressCategory mAvailableDevicesCategory;
@Nullable
private volatile BluetoothDevice mJustBonded = null;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@VisibleForTesting
@Nullable
ProgressDialogFragment mProgressDialog = null;
@VisibleForTesting
boolean mShouldTriggerAudioSharingShareThenPairFlow = false;
private CopyOnWriteArrayList<BluetoothDevice> mDevicesWithMetadataChangedListener =
new CopyOnWriteArrayList<>();
// BluetoothDevicePreference updates the summary based on several callbacks, including
// BluetoothAdapter.OnMetadataChangedListener and BluetoothCallback. In most cases,
// metadata changes callback will be triggered before onDeviceBondStateChanged(BOND_BONDED).
// And before we hear onDeviceBondStateChanged(BOND_BONDED), the BluetoothDevice.getState() has
// already been BOND_BONDED. These event sequence will lead to: before we hear
// onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already
// change from "Pairing..." to empty since it listens to metadata changes happens earlier.
//
// In share then pair flow, we have to wait on this page till the device is connected.
// The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and
// "Connecting..." To help users better understand the process, we listen to metadata change
// as well and show a progress dialog with "Connecting to ...." once BluetoothDevice.getState()
// gets to BOND_BONDED.
final BluetoothAdapter.OnMetadataChangedListener mMetadataListener =
new BluetoothAdapter.OnMetadataChangedListener() {
@Override
public void onMetadataChanged(@NonNull BluetoothDevice device, int key,
@Nullable byte[] value) {
Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key);
if (mShouldTriggerAudioSharingShareThenPairFlow && mProgressDialog == null
&& device.getBondState() == BluetoothDevice.BOND_BONDED
&& mSelectedList.contains(device)) {
triggerAudioSharingShareThenPairFlow(device);
// Once device is bonded, remove the listener
removeOnMetadataChangedListener(device);
}
}
};
public BluetoothDevicePairingDetailBase() {
super(DISALLOW_CONFIG_BLUETOOTH);
}
@Override
public void initPreferencesFromPreferenceScreen() {
mAvailableDevicesCategory = findPreference(getDeviceListKey());
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
mInitialScanStarted = false;
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
if (mLocalManager == null) {
Log.e(getLogTag(), "Bluetooth is not supported on this device");
return;
}
updateBluetooth();
mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow();
}
@Override
public void onStop() {
super.onStop();
if (mLocalManager == null) {
Log.e(getLogTag(), "Bluetooth is not supported on this device");
return;
}
disableScanning();
}
@Override
public void onDestroy() {
super.onDestroy();
var unused = ThreadUtils.postOnBackgroundThread(() -> {
mDevicesWithMetadataChangedListener.forEach(
device -> {
try {
if (mBluetoothAdapter != null) {
mBluetoothAdapter.removeOnMetadataChangedListener(device,
mMetadataListener);
mDevicesWithMetadataChangedListener.remove(device);
}
} catch (IllegalArgumentException e) {
Log.d(getLogTag(), "Fail to remove listener: " + e);
}
});
mDevicesWithMetadataChangedListener.clear();
});
}
@Override
public void onBluetoothStateChanged(int bluetoothState) {
super.onBluetoothStateChanged(bluetoothState);
updateContent(bluetoothState);
if (bluetoothState == BluetoothAdapter.STATE_ON) {
showBluetoothTurnedOnToast();
}
}
@Override
public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
if (bondState == BluetoothDevice.BOND_BONDED) {
if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) {
BluetoothDevice device = cachedDevice.getDevice();
if (device != null && mSelectedList.contains(device)) {
triggerAudioSharingShareThenPairFlow(device);
removeOnMetadataChangedListener(device);
return;
}
}
// If one device is connected(bonded), then close this fragment.
finish();
return;
} else if (bondState == BluetoothDevice.BOND_BONDING) {
if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) {
BluetoothDevice device = cachedDevice.getDevice();
if (device != null && mSelectedList.contains(device)) {
addOnMetadataChangedListener(device);
}
}
// Set the bond entry where binding process starts for logging hearing aid device info
final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
.getAttribution(getActivity());
final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry(
pageId);
HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice);
} else if (bondState == BluetoothDevice.BOND_NONE) {
if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) {
BluetoothDevice device = cachedDevice.getDevice();
if (device != null && mSelectedList.contains(device)) {
removeOnMetadataChangedListener(device);
}
}
}
if (mSelectedDevice != null && cachedDevice != null) {
BluetoothDevice device = cachedDevice.getDevice();
if (device != null && mSelectedDevice.equals(device)
&& bondState == BluetoothDevice.BOND_NONE) {
// If currently selected device failed to bond, restart scanning
enableScanning();
}
}
}
@Override
public void onProfileConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, @ConnectionState int state,
int bluetoothProfile) {
// This callback is used to handle the case that bonded device is connected in pairing list.
// 1. If user selected multiple bonded devices in pairing list, after connected
// finish this page.
// 2. If the bonded devices auto connected in paring list, after connected it will be
// removed from paring list.
if (cachedDevice != null && cachedDevice.isConnected()) {
final BluetoothDevice device = cachedDevice.getDevice();
if (device != null
&& mSelectedList.contains(device)) {
if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) {
if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& state == BluetoothAdapter.STATE_CONNECTED
&& device.equals(mJustBonded)
&& mShouldTriggerAudioSharingShareThenPairFlow) {
Log.d(getLogTag(),
"onProfileConnectionStateChanged, assistant profile connected");
dismissConnectingDialog();
mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID);
finishFragmentWithResultForAudioSharing(device);
}
} else {
finish();
}
} else {
onDeviceDeleted(cachedDevice);
}
}
}
@Override
public void enableScanning() {
// Clear all device states before first scan
if (!mInitialScanStarted) {
if (mAvailableDevicesCategory != null) {
removeAllDevices();
}
mLocalManager.getCachedDeviceManager().clearNonBondedDevices();
mInitialScanStarted = true;
}
super.enableScanning();
}
@Override
public void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
disableScanning();
super.onDevicePreferenceClick(btPreference);
// Clean up the previous bond value
mJustBonded = null;
}
@VisibleForTesting
void updateBluetooth() {
if (mBluetoothAdapter.isEnabled()) {
updateContent(mBluetoothAdapter.getState());
} else {
// Turn on bluetooth if it is disabled
mBluetoothAdapter.enable();
}
}
/**
* Enables the scanning when {@code bluetoothState} is on, or finish the page when
* {@code bluetoothState} is off.
*
* @param bluetoothState the current Bluetooth state, the possible values that will handle here:
* {@link android.bluetooth.BluetoothAdapter#STATE_OFF},
* {@link android.bluetooth.BluetoothAdapter#STATE_ON},
*/
@VisibleForTesting
public void updateContent(int bluetoothState) {
switch (bluetoothState) {
case BluetoothAdapter.STATE_ON:
mBluetoothAdapter.enable();
enableScanning();
break;
case BluetoothAdapter.STATE_OFF:
finish();
break;
}
}
@VisibleForTesting
void showBluetoothTurnedOnToast() {
Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
Toast.LENGTH_SHORT).show();
}
@VisibleForTesting
boolean shouldTriggerAudioSharingShareThenPairFlow() {
if (BluetoothUtils.isAudioSharingUIAvailable(getContext())) {
Activity activity = getActivity();
Intent intent = activity == null ? null : activity.getIntent();
Bundle args =
intent == null ? null :
intent.getBundleExtra(
SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
return args != null
&& args.getBoolean(EXTRA_PAIR_AND_JOIN_SHARING, false);
}
return false;
}
private void addOnMetadataChangedListener(@Nullable BluetoothDevice device) {
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (mBluetoothAdapter != null && device != null
&& !mDevicesWithMetadataChangedListener.contains(device)) {
mBluetoothAdapter.addOnMetadataChangedListener(device, mExecutor,
mMetadataListener);
mDevicesWithMetadataChangedListener.add(device);
}
});
}
private void removeOnMetadataChangedListener(@Nullable BluetoothDevice device) {
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (mBluetoothAdapter != null && device != null
&& mDevicesWithMetadataChangedListener.contains(device)) {
try {
mBluetoothAdapter.removeOnMetadataChangedListener(device, mMetadataListener);
mDevicesWithMetadataChangedListener.remove(device);
} catch (IllegalArgumentException e) {
Log.d(getLogTag(), "Fail to remove listener: " + e);
}
}
});
}
private void triggerAudioSharingShareThenPairFlow(
@NonNull BluetoothDevice device) {
var unused = ThreadUtils.postOnBackgroundThread(() -> {
if (mJustBonded != null) {
Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done");
return;
}
mJustBonded = device;
// Show connecting device progress
String aliasName = device.getAlias();
String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress()
: aliasName;
showConnectingDialog(deviceName);
// Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio
// sharing.
if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) {
mHandler.postDelayed(() ->
postOnMainThread(
() -> {
Log.d(getLogTag(), "Show incompatible dialog when timeout");
dismissConnectingDialog();
AudioSharingIncompatibleDialogFragment.show(this, deviceName,
() -> finish());
}), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS);
}
});
}
private void finishFragmentWithResultForAudioSharing(@Nullable BluetoothDevice device) {
Intent resultIntent = new Intent();
resultIntent.putExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device);
if (getActivity() != null) {
getActivity().setResult(Activity.RESULT_OK, resultIntent);
}
finish();
}
private void showConnectingDialog(@NonNull String deviceName) {
postOnMainThread(() -> {
String message = getContext().getString(R.string.progress_dialog_connect_device_content,
deviceName);
if (mProgressDialog == null) {
mProgressDialog = ProgressDialogFragment.newInstance(this);
}
if (mProgressDialog != null) {
mProgressDialog.show(message);
}
});
}
private void dismissConnectingDialog() {
postOnMainThread(() -> {
if (mProgressDialog != null) {
Log.d(getLogTag(), "Dismiss connecting dialog.");
mProgressDialog.dismissAllowingStateLoss();
}
});
}
private void postOnMainThread(@NonNull Runnable runnable) {
getContext().getMainExecutor().execute(runnable);
}
}