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

Commit 723c385c authored by jasonwshsu's avatar jasonwshsu
Browse files

[Pair hearing devices] Extract common behavior in BluetoothPairingDetail into Base class

* Before adding new hearing device feature, we will extract the common part into Base class first. They will share most of the UI component.

Bug: 237625815
Test: make RunSettingsRoboTests ROBOTEST_FILTER=BluetoothDevicePairingDetailBaseTest
Test: make RunSettingsRoboTests ROBOTEST_FILTER=BluetoothDevicePairingDetailTest
Change-Id: I3a44c4c464d630fdcafa151afc82d3000fd728a3
parent 23417c3a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@

    <com.android.settings.bluetooth.BluetoothProgressCategory
        android:key="available_devices"
        android:title="@string/bluetooth_paired_device_title"/>
        android:title="@string/bluetooth_preference_found_media_devices"/>

    <com.android.settingslib.widget.FooterPreference/>

+200 −0
Original line number Diff line number Diff line
/*
 * 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 android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
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.accessibility.AccessibilityStatsLogUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;

/**
 * Abstract class for providing basic interaction for a list of Bluetooth devices in bluetooth
 * device pairing detail page.
 */
public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment {

    protected boolean mInitialScanStarted;
    @VisibleForTesting
    protected BluetoothProgressCategory mAvailableDevicesCategory;

    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();
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mLocalManager == null) {
            Log.e(getLogTag(), "Bluetooth is not supported on this device");
            return;
        }
        disableScanning();
    }

    @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 one device is connected(bonded), then close this fragment.
            finish();
            return;
        } else if (bondState == BluetoothDevice.BOND_BONDING) {
            // Set the bond entry where binding process starts for logging hearing aid device info
            final int pageId = FeatureFactory.getFactory(
                    getContext()).getMetricsFeatureProvider().getAttribution(getActivity());
            final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry(
                    pageId);
            HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice);
        }
        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(CachedBluetoothDevice cachedDevice, 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)) {
                finish();
            } else if (mDevicePreferenceMap.containsKey(cachedDevice)) {
                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);
    }

    @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:
                mDevicePreferenceMap.clear();
                clearPreferenceGroupCache();
                mBluetoothAdapter.enable();
                enableScanning();
                break;

            case BluetoothAdapter.STATE_OFF:
                finish();
                break;
        }
    }

    /**
     * Clears all cached preferences in {@code preferenceGroup}.
     */
    private void clearPreferenceGroupCache() {
        cacheRemoveAllPrefs(mAvailableDevicesCategory);
        removeCachedPrefs(mAvailableDevicesCategory);
    }

    @VisibleForTesting
    void showBluetoothTurnedOnToast() {
        Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
                Toast.LENGTH_SHORT).show();
    }
}
+32 −139
Original line number Diff line number Diff line
@@ -16,31 +16,25 @@

package com.android.settings.bluetooth;

import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;

import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.settings.R;
import com.android.settings.accessibility.AccessibilityStatsLogUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
import com.android.settingslib.search.Indexable;
import com.android.settingslib.widget.FooterPreference;

/**
 * BluetoothPairingDetail is a page to scan bluetooth devices and pair them.
 */
public class BluetoothPairingDetail extends DeviceListPreferenceFragment implements
public class BluetoothPairingDetail extends BluetoothDevicePairingDetailBase implements
        Indexable {
    private static final String TAG = "BluetoothPairingDetail";

@@ -49,69 +43,44 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme
    @VisibleForTesting
    static final String KEY_FOOTER_PREF = "footer_preference";

    @VisibleForTesting
    BluetoothProgressCategory mAvailableDevicesCategory;
    @VisibleForTesting
    FooterPreference mFooterPreference;
    @VisibleForTesting
    AlwaysDiscoverable mAlwaysDiscoverable;

    private boolean mInitialScanStarted;

    public BluetoothPairingDetail() {
        super(DISALLOW_CONFIG_BLUETOOTH);
        super();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mInitialScanStarted = false;
    public void onAttach(Context context) {
        super.onAttach(context);
        use(BluetoothDeviceRenamePreferenceController.class).setFragment(this);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mAlwaysDiscoverable = new AlwaysDiscoverable(getContext());
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mLocalManager == null){
            Log.e(TAG, "Bluetooth is not supported on this device");
            return;
        }
        updateBluetooth();
        mAvailableDevicesCategory.setProgress(mBluetoothAdapter.isDiscovering());
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        use(BluetoothDeviceRenamePreferenceController.class).setFragment(this);
    }

    @VisibleForTesting
    void updateBluetooth() {
        if (mBluetoothAdapter.isEnabled()) {
            updateContent(mBluetoothAdapter.getState());
        } else {
            // Turn on bluetooth if it is disabled
            mBluetoothAdapter.enable();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mLocalManager == null){
            Log.e(TAG, "Bluetooth is not supported on this device");
            return;
        }
        // Make the device only visible to connected devices.
        mAlwaysDiscoverable.stop();
        disableScanning();
    }

    @Override
    void initPreferencesFromPreferenceScreen() {
        mAvailableDevicesCategory = (BluetoothProgressCategory) findPreference(KEY_AVAIL_DEVICES);
        mFooterPreference = (FooterPreference) findPreference(KEY_FOOTER_PREF);
    public void initPreferencesFromPreferenceScreen() {
        super.initPreferencesFromPreferenceScreen();
        mFooterPreference = findPreference(KEY_FOOTER_PREF);
        mFooterPreference.setSelectable(false);
    }

@@ -120,23 +89,25 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme
        return SettingsEnums.BLUETOOTH_PAIRING;
    }

    /**
     * {@inheritDoc}
     *
     * Will update footer and keep the device discoverable as long as the page is visible.
     */
    @VisibleForTesting
    @Override
    void enableScanning() {
        // Clear all device states before first scan
        if (!mInitialScanStarted) {
            if (mAvailableDevicesCategory != null) {
                removeAllDevices();
    public void updateContent(int bluetoothState) {
        super.updateContent(bluetoothState);
        if (bluetoothState == BluetoothAdapter.STATE_ON) {
            if (mInitialScanStarted) {
                // Don't show bonded devices when screen turned back on
                setFilter(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER);
                addCachedDevices();
            }
            mLocalManager.getCachedDeviceManager().clearNonBondedDevices();
            mInitialScanStarted = true;
            setFilter(BluetoothDeviceFilter.ALL_FILTER);
            updateFooterPreference(mFooterPreference);
            mAlwaysDiscoverable.start();
        }
        super.enableScanning();
    }

    @Override
    void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
        disableScanning();
        super.onDevicePreferenceClick(btPreference);
    }

    @Override
@@ -146,78 +117,6 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme
        mAvailableDevicesCategory.setProgress(started);
    }

    @VisibleForTesting
    void updateContent(int bluetoothState) {
        switch (bluetoothState) {
            case BluetoothAdapter.STATE_ON:
                mDevicePreferenceMap.clear();
                mBluetoothAdapter.enable();

                addDeviceCategory(mAvailableDevicesCategory,
                        R.string.bluetooth_preference_found_media_devices,
                        BluetoothDeviceFilter.ALL_FILTER, mInitialScanStarted);
                updateFooterPreference(mFooterPreference);
                mAlwaysDiscoverable.start();
                enableScanning();
                break;

            case BluetoothAdapter.STATE_OFF:
                finish();
                break;
        }
    }

    @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 one device is connected(bonded), then close this fragment.
            finish();
            return;
        } else if (bondState == BluetoothDevice.BOND_BONDING) {
            // Set the bond entry where binding process starts for logging hearing aid device info
            final int pageId = FeatureFactory.getFactory(
                    getContext()).getMetricsFeatureProvider().getAttribution(getActivity());
            final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry(
                    pageId);
            HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice);
        }
        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(CachedBluetoothDevice cachedDevice, 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)) {
                finish();
            } else if (mDevicePreferenceMap.containsKey(cachedDevice)) {
                onDeviceDeleted(cachedDevice);
            }
        }
    }

    @Override
    public int getHelpResource() {
        return R.string.help_url_bluetooth;
@@ -237,10 +136,4 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme
    public String getDeviceListKey() {
        return KEY_AVAIL_DEVICES;
    }

    @VisibleForTesting
    void showBluetoothTurnedOnToast() {
        Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
                Toast.LENGTH_SHORT).show();
    }
}
+266 −0

File added.

Preview size limit exceeded, changes collapsed.

+20 −193

File changed.

Preview size limit exceeded, changes collapsed.