Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 133fcf3d authored by Yiyi Shen's avatar Yiyi Shen Committed by Android (Google) Code Review
Browse files

Merge "[Audiosharing] Refine share then pair flow" into main

parents 4f6b5627 800f81c8
Loading
Loading
Loading
Loading
+244 −5
Original line number Diff line number Diff line
@@ -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)) {
            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
@@ -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);
    }
}
+44 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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();
@@ -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();
@@ -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() {
+33 −15

File changed.

Preview size limit exceeded, changes collapsed.

+2 −3
Original line number Diff line number Diff line
@@ -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";
@@ -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;
@@ -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);
+20 −4
Original line number Diff line number Diff line
@@ -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());
@@ -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