Merge "[Audiosharing] Refine share then pair flow" into main
This commit is contained in:
@@ -18,32 +18,94 @@ 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.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
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();
|
||||
@Nullable
|
||||
private AlertDialog mLoadingDialog = 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 loading 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 && mLoadingDialog == 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);
|
||||
@@ -68,6 +130,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
return;
|
||||
}
|
||||
updateBluetooth();
|
||||
mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,6 +143,26 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
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);
|
||||
@@ -92,16 +175,37 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
@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();
|
||||
@@ -114,7 +218,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state,
|
||||
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
|
||||
@@ -123,8 +228,22 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
// removed from paring list.
|
||||
if (cachedDevice != null && cachedDevice.isConnected()) {
|
||||
final BluetoothDevice device = cachedDevice.getDevice();
|
||||
if (device != null && mSelectedList.contains(device)) {
|
||||
finish();
|
||||
if (device != null
|
||||
&& mSelectedList.contains(device)) {
|
||||
if (!BluetoothUtils.isAudioSharingEnabled()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
onDeviceDeleted(cachedDevice);
|
||||
}
|
||||
@@ -148,6 +267,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
public void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
|
||||
disableScanning();
|
||||
super.onDevicePreferenceClick(btPreference);
|
||||
// Clean up the previous bond value
|
||||
mJustBonded = null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -165,8 +286,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
* {@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},
|
||||
* {@link android.bluetooth.BluetoothAdapter#STATE_OFF},
|
||||
* {@link android.bluetooth.BluetoothAdapter#STATE_ON},
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void updateContent(int bluetoothState) {
|
||||
@@ -187,4 +308,122 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
|
||||
Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean shouldTriggerAudioSharingShareThenPairFlow() {
|
||||
if (!BluetoothUtils.isAudioSharingEnabled()) return false;
|
||||
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);
|
||||
}
|
||||
|
||||
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 loading state
|
||||
String aliasName = device.getAlias();
|
||||
String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress()
|
||||
: aliasName;
|
||||
showConnectingDialog("Connecting to " + 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();
|
||||
}
|
||||
|
||||
// TODO: use DialogFragment
|
||||
private void showConnectingDialog(@NonNull String message) {
|
||||
postOnMainThread(() -> {
|
||||
if (mLoadingDialog != null) {
|
||||
Log.d(getLogTag(), "showConnectingDialog, is already showing");
|
||||
TextView textView = mLoadingDialog.findViewById(R.id.message);
|
||||
if (textView != null && !message.equals(textView.getText().toString())) {
|
||||
Log.d(getLogTag(), "showConnectingDialog, update message");
|
||||
// TODO: use string res once finalized
|
||||
textView.setText(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Log.d(getLogTag(), "showConnectingDialog, show dialog");
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
LayoutInflater inflater = LayoutInflater.from(builder.getContext());
|
||||
View customView = inflater.inflate(
|
||||
R.layout.dialog_audio_sharing_loading_state, /* root= */
|
||||
null);
|
||||
TextView textView = customView.findViewById(R.id.message);
|
||||
if (textView != null) {
|
||||
// TODO: use string res once finalized
|
||||
textView.setText(message);
|
||||
}
|
||||
AlertDialog dialog = builder.setView(customView).setCancelable(false).create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
mLoadingDialog = dialog;
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
|
||||
private void dismissConnectingDialog() {
|
||||
postOnMainThread(() -> {
|
||||
if (mLoadingDialog != null) {
|
||||
mLoadingDialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void postOnMainThread(@NonNull Runnable runnable) {
|
||||
getContext().getMainExecutor().execute(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user