Loading src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +244 −5 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -68,6 +130,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); } @Override Loading @@ -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); Loading @@ -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(); Loading @@ -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 Loading @@ -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)) { 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); } Loading @@ -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 Loading Loading @@ -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); } } src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java +44 −4 Original line number Diff line number Diff line Loading @@ -16,10 +16,18 @@ package com.android.settings.connecteddevice.audiosharing; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; import android.app.Activity; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; Loading @@ -27,16 +35,21 @@ import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.SettingsMainSwitchBar; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.utils.ThreadUtils; public class AudioSharingDashboardFragment extends DashboardFragment implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener { private static final String TAG = "AudioSharingDashboardFrag"; public static final int SHARE_THEN_PAIR_REQUEST_CODE = 1002; SettingsMainSwitchBar mMainSwitchBar; private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController; private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController; private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController; private AudioStreamsCategoryController mAudioStreamsCategoryController; private AudioSharingSwitchBarController mAudioSharingSwitchBarController; public AudioSharingDashboardFragment() { super(); Loading Loading @@ -84,13 +97,38 @@ public class AudioSharingDashboardFragment extends DashboardFragment final SettingsActivity activity = (SettingsActivity) getActivity(); mMainSwitchBar = activity.getSwitchBar(); mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title)); AudioSharingSwitchBarController switchBarController = mAudioSharingSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this); switchBarController.init(this); getSettingsLifecycle().addObserver(switchBarController); mAudioSharingSwitchBarController.init(this); getSettingsLifecycle().addObserver(mAudioSharingSwitchBarController); mMainSwitchBar.show(); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (!BluetoothUtils.isAudioSharingEnabled()) return; // In share then pair flow, after users be routed to pair new device page and successfully // pair and connect an LEA headset, the pair fragment will be finished with RESULT_OK // and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar controller, // which is responsible for adding source to the device with loading indicator. if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK) { BluetoothDevice btDevice = data != null ? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, BluetoothDevice.class) : null; Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice); if (btDevice != null) { var unused = ThreadUtils.postOnBackgroundThread( () -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair( btDevice)); } } } } @Override public void onAudioSharingStateChanged() { updateVisibilityForAttachedPreferences(); Loading @@ -107,11 +145,13 @@ public class AudioSharingDashboardFragment extends DashboardFragment AudioSharingDeviceVolumeGroupController volumeGroupController, AudioSharingCallAudioPreferenceController callAudioController, AudioSharingPlaySoundPreferenceController playSoundController, AudioStreamsCategoryController streamsCategoryController) { AudioStreamsCategoryController streamsCategoryController, AudioSharingSwitchBarController switchBarController) { mAudioSharingDeviceVolumeGroupController = volumeGroupController; mAudioSharingCallAudioPreferenceController = callAudioController; mAudioSharingPlaySoundPreferenceController = playSoundController; mAudioStreamsCategoryController = streamsCategoryController; mAudioSharingSwitchBarController = switchBarController; } private void updateVisibilityForAttachedPreferences() { Loading src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +33 −15 File changed.Preview size limit exceeded, changes collapsed. Show changes src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java +2 −3 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingIncompatDlg"; Loading Loading @@ -59,7 +58,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr * * @param host The Fragment this dialog will be hosted. */ public static void show(@Nullable Fragment host, @NonNull CachedBluetoothDevice cachedDevice, public static void show(@Nullable Fragment host, @NonNull String deviceName, @NonNull DialogEventListener listener) { if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return; final FragmentManager manager; Loading @@ -77,7 +76,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr } Log.d(TAG, "Show up the incompatible device dialog."); final Bundle bundle = new Bundle(); bundle.putString(BUNDLE_KEY_DEVICE_NAME, cachedDevice.getName()); bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName); AudioSharingIncompatibleDialogFragment dialogFrag = new AudioSharingIncompatibleDialogFragment(); dialogFrag.setArguments(bundle); Loading src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java +20 −4 Original line number Diff line number Diff line Loading @@ -115,10 +115,6 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr @NonNull public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mHandler = new Handler(Looper.getMainLooper()); mHandler.postDelayed(() -> { Log.d(TAG, "Auto dismiss dialog after timeout"); dismiss(); }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); Bundle args = requireArguments(); String message = args.getString(BUNDLE_KEY_MESSAGE, ""); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); Loading @@ -132,6 +128,26 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr return dialog; } @Override public void onStart() { super.onStart(); if (mHandler != null) { Log.d(TAG, "onStart, postTimeOut for auto dismiss"); mHandler.postDelayed(() -> { Log.d(TAG, "Try to auto dismiss dialog after timeout"); try { Dialog dialog = getDialog(); if (dialog != null) { Log.d(TAG, "Dialog is not null, dismiss"); dismissAllowingStateLoss(); } } catch (IllegalStateException e) { Log.d(TAG, "Fail to dismiss: " + e.getMessage()); } }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); } } @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); Loading Loading
src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +244 −5 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -68,6 +130,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); } @Override Loading @@ -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); Loading @@ -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(); Loading @@ -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 Loading @@ -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)) { 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); } Loading @@ -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 Loading Loading @@ -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); } }
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java +44 −4 Original line number Diff line number Diff line Loading @@ -16,10 +16,18 @@ package com.android.settings.connecteddevice.audiosharing; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; import android.app.Activity; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; Loading @@ -27,16 +35,21 @@ import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.SettingsMainSwitchBar; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.utils.ThreadUtils; public class AudioSharingDashboardFragment extends DashboardFragment implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener { private static final String TAG = "AudioSharingDashboardFrag"; public static final int SHARE_THEN_PAIR_REQUEST_CODE = 1002; SettingsMainSwitchBar mMainSwitchBar; private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController; private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController; private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController; private AudioStreamsCategoryController mAudioStreamsCategoryController; private AudioSharingSwitchBarController mAudioSharingSwitchBarController; public AudioSharingDashboardFragment() { super(); Loading Loading @@ -84,13 +97,38 @@ public class AudioSharingDashboardFragment extends DashboardFragment final SettingsActivity activity = (SettingsActivity) getActivity(); mMainSwitchBar = activity.getSwitchBar(); mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title)); AudioSharingSwitchBarController switchBarController = mAudioSharingSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this); switchBarController.init(this); getSettingsLifecycle().addObserver(switchBarController); mAudioSharingSwitchBarController.init(this); getSettingsLifecycle().addObserver(mAudioSharingSwitchBarController); mMainSwitchBar.show(); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (!BluetoothUtils.isAudioSharingEnabled()) return; // In share then pair flow, after users be routed to pair new device page and successfully // pair and connect an LEA headset, the pair fragment will be finished with RESULT_OK // and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar controller, // which is responsible for adding source to the device with loading indicator. if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK) { BluetoothDevice btDevice = data != null ? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, BluetoothDevice.class) : null; Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice); if (btDevice != null) { var unused = ThreadUtils.postOnBackgroundThread( () -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair( btDevice)); } } } } @Override public void onAudioSharingStateChanged() { updateVisibilityForAttachedPreferences(); Loading @@ -107,11 +145,13 @@ public class AudioSharingDashboardFragment extends DashboardFragment AudioSharingDeviceVolumeGroupController volumeGroupController, AudioSharingCallAudioPreferenceController callAudioController, AudioSharingPlaySoundPreferenceController playSoundController, AudioStreamsCategoryController streamsCategoryController) { AudioStreamsCategoryController streamsCategoryController, AudioSharingSwitchBarController switchBarController) { mAudioSharingDeviceVolumeGroupController = volumeGroupController; mAudioSharingCallAudioPreferenceController = callAudioController; mAudioSharingPlaySoundPreferenceController = playSoundController; mAudioStreamsCategoryController = streamsCategoryController; mAudioSharingSwitchBarController = switchBarController; } private void updateVisibilityForAttachedPreferences() { Loading
src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +33 −15 File changed.Preview size limit exceeded, changes collapsed. Show changes
src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java +2 −3 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingIncompatDlg"; Loading Loading @@ -59,7 +58,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr * * @param host The Fragment this dialog will be hosted. */ public static void show(@Nullable Fragment host, @NonNull CachedBluetoothDevice cachedDevice, public static void show(@Nullable Fragment host, @NonNull String deviceName, @NonNull DialogEventListener listener) { if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return; final FragmentManager manager; Loading @@ -77,7 +76,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr } Log.d(TAG, "Show up the incompatible device dialog."); final Bundle bundle = new Bundle(); bundle.putString(BUNDLE_KEY_DEVICE_NAME, cachedDevice.getName()); bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName); AudioSharingIncompatibleDialogFragment dialogFrag = new AudioSharingIncompatibleDialogFragment(); dialogFrag.setArguments(bundle); Loading
src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java +20 −4 Original line number Diff line number Diff line Loading @@ -115,10 +115,6 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr @NonNull public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mHandler = new Handler(Looper.getMainLooper()); mHandler.postDelayed(() -> { Log.d(TAG, "Auto dismiss dialog after timeout"); dismiss(); }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); Bundle args = requireArguments(); String message = args.getString(BUNDLE_KEY_MESSAGE, ""); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); Loading @@ -132,6 +128,26 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr return dialog; } @Override public void onStart() { super.onStart(); if (mHandler != null) { Log.d(TAG, "onStart, postTimeOut for auto dismiss"); mHandler.postDelayed(() -> { Log.d(TAG, "Try to auto dismiss dialog after timeout"); try { Dialog dialog = getDialog(); if (dialog != null) { Log.d(TAG, "Dialog is not null, dismiss"); dismissAllowingStateLoss(); } } catch (IllegalStateException e) { Log.d(TAG, "Fail to dismiss: " + e.getMessage()); } }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); } } @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); Loading