New hearing device pairing page (1/2)

Rewrite a new hearing device pairing page with update UI for "See more
devices".

Bug: 307473972
Test: atest HearingDevicePairingFragmentTest
Test: flip the flag com.android.settings.flags.new_hearing_device_pairing_page && atest HearingAidPairingDialogFragmentTest AddDevicePreferenceControllerTest
Change-Id: Ic60601905e3d0d7d7c5b1ef9733652118a211f1d
diff --git a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
index acdce96..5a464b5 100644
--- a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
+++ b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
@@ -32,7 +32,7 @@
 flag {
     name: "new_hearing_device_pairing_page"
     namespace: "accessibility"
-    description: "New hearing device pairing page with deny list method"
+    description: "New hearing device pairing page with extra MFi+ASHA filtering"
     bug: "307473972"
 }
 
diff --git a/res/layout/arrow_preference.xml b/res/layout/arrow_preference.xml
new file mode 100644
index 0000000..0924a44
--- /dev/null
+++ b/res/layout/arrow_preference.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingVertical="@dimen/settingslib_switchbar_margin"
+    android:background="@android:color/transparent">
+
+    <LinearLayout
+        android:id="@+id/background"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:paddingStart="@dimen/settingslib_switchbar_padding_left"
+        android:paddingEnd="@dimen/settingslib_switchbar_padding_right"
+        android:background="@drawable/settingslib_switch_bar_bg_on"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_height="wrap_content"
+            android:layout_width="0dp"
+            android:layout_gravity="start|center_vertical"
+            android:layout_weight="1"
+            android:paddingVertical="@dimen/settingslib_switch_title_margin"
+            android:ellipsize="end"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:hyphenationFrequency="normalFast"
+            android:lineBreakWordStyle="phrase"
+            style="@style/MainSwitchText.Settingslib"/>
+
+        <ImageView
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="center_vertical"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_arrow_forward"/>
+
+    </LinearLayout>
+</FrameLayout>
diff --git a/res/xml/accessibility_hearing_aids.xml b/res/xml/accessibility_hearing_aids.xml
index 20c8e29..57a0fe2 100644
--- a/res/xml/accessibility_hearing_aids.xml
+++ b/res/xml/accessibility_hearing_aids.xml
@@ -28,11 +28,10 @@
         settings:controller="com.android.settings.accessibility.AvailableHearingDevicePreferenceController"/>
 
     <com.android.settingslib.RestrictedPreference
-        android:key="add_bt_devices"
+        android:key="hearing_device_add_bt_devices"
         android:title="@string/bluetooth_pairing_pref_title"
         android:icon="@drawable/ic_add_24dp"
         android:summary="@string/connected_device_add_device_summary"
-        android:fragment="com.android.settings.accessibility.HearingDevicePairingDetail"
         settings:userRestriction="no_config_bluetooth"
         settings:useAdminDisabledSummary="true"
         settings:controller="com.android.settings.connecteddevice.AddDevicePreferenceController"/>
diff --git a/res/xml/hearing_device_pairing_fragment.xml b/res/xml/hearing_device_pairing_fragment.xml
new file mode 100644
index 0000000..1ccc1dd
--- /dev/null
+++ b/res/xml/hearing_device_pairing_fragment.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/bluetooth_pairing_pref_title">
+
+    <com.android.settings.bluetooth.BluetoothProgressCategory
+        android:key="available_hearing_devices"
+        android:title="@string/accessibility_found_hearing_devices" />
+
+    <PreferenceCategory
+        android:key="more_devices_category"
+        android:title="@string/accessibility_found_all_devices">
+        <com.android.settings.accessibility.ArrowPreference
+            android:key="more_devices"
+            android:title="@string/accessibility_list_all_devices_title"
+            settings:searchable="false"
+            settings:userRestriction="no_config_bluetooth"
+            settings:useAdminDisabledSummary="true"
+            settings:controller="com.android.settings.accessibility.ViewAllBluetoothDevicesPreferenceController"/>
+    </PreferenceCategory>
+
+    <com.android.settings.accessibility.AccessibilityFooterPreference
+        android:key="hearing_device_footer"
+        android:title="@string/accessibility_hearing_device_footer_summary"
+        android:selectable="false"
+        settings:searchable="false"
+        settings:controller="com.android.settings.accessibility.PairHearingDeviceFooterPreferenceController"/>
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
index 33fef62..80a03c6 100644
--- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
+++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
@@ -36,9 +36,9 @@
 /** Accessibility settings for hearing aids. */
 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
 public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPreferenceFragment {
-
     private static final String TAG = "AccessibilityHearingAidsFragment";
     private static final String KEY_HEARING_OPTIONS_CATEGORY = "hearing_options_category";
+    public static final String KEY_HEARING_DEVICE_ADD_BT_DEVICES = "hearing_device_add_bt_devices";
     private static final int SHORTCUT_PREFERENCE_IN_CATEGORY_INDEX = 20;
     private String mFeatureName;
 
diff --git a/src/com/android/settings/accessibility/ArrowPreference.java b/src/com/android/settings/accessibility/ArrowPreference.java
new file mode 100644
index 0000000..32e2bcb
--- /dev/null
+++ b/src/com/android/settings/accessibility/ArrowPreference.java
@@ -0,0 +1,58 @@
+/*
+ * 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.accessibility;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+
+/**
+ * A settings preference with colored rounded rectangle background and an arrow icon on the right
+ */
+public class ArrowPreference extends Preference {
+
+    public ArrowPreference(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public ArrowPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, TypedArrayUtils.getAttr(context,
+                androidx.preference.R.attr.preferenceStyle,
+                android.R.attr.preferenceStyle));
+    }
+
+    public ArrowPreference(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ArrowPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init();
+    }
+
+    private void init() {
+        setLayoutResource(R.layout.arrow_preference);
+    }
+}
diff --git a/src/com/android/settings/accessibility/HearingDevicePairingFragment.java b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java
new file mode 100644
index 0000000..ffb5960
--- /dev/null
+++ b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java
@@ -0,0 +1,397 @@
+/*
+ * 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.accessibility;
+
+import static android.app.Activity.RESULT_OK;
+import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
+
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.BluetoothDevicePreference;
+import com.android.settings.bluetooth.BluetoothProgressCategory;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.dashboard.RestrictedDashboardFragment;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.HearingAidInfo;
+import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This fragment shows all scanned hearing devices through BLE scanning. Users can
+ * pair them in this page.
+ */
+public class HearingDevicePairingFragment extends RestrictedDashboardFragment implements
+        BluetoothCallback {
+
+    private static final boolean DEBUG = true;
+    private static final String TAG = "HearingDevicePairingFragment";
+    private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
+            "persist.bluetooth.showdeviceswithoutnames";
+    private static final String KEY_AVAILABLE_HEARING_DEVICES = "available_hearing_devices";
+
+    LocalBluetoothManager mLocalManager;
+    @Nullable
+    BluetoothAdapter mBluetoothAdapter;
+    @Nullable
+    CachedBluetoothDeviceManager mCachedDeviceManager;
+
+    private boolean mShowDevicesWithoutNames;
+    @Nullable
+    private BluetoothProgressCategory mAvailableHearingDeviceGroup;
+
+    @Nullable
+    BluetoothDevice mSelectedDevice;
+    final List<BluetoothDevice> mSelectedDeviceList = new ArrayList<>();
+    final Map<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
+            new HashMap<>();
+
+    private List<ScanFilter> mLeScanFilters;
+
+    public HearingDevicePairingFragment() {
+        super(DISALLOW_CONFIG_BLUETOOTH);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mLocalManager = Utils.getLocalBtManager(getActivity());
+        if (mLocalManager == null) {
+            Log.e(TAG, "Bluetooth is not supported on this device");
+            return;
+        }
+        mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
+        mCachedDeviceManager = mLocalManager.getCachedDeviceManager();
+        mShowDevicesWithoutNames = SystemProperties.getBoolean(
+                BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
+
+        initPreferencesFromPreferenceScreen();
+        initHearingDeviceLeScanFilters();
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        use(ViewAllBluetoothDevicesPreferenceController.class).init(this);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (mLocalManager == null || mBluetoothAdapter == null || isUiRestricted()) {
+            return;
+        }
+        mLocalManager.setForegroundActivity(getActivity());
+        mLocalManager.getEventManager().registerCallback(this);
+        if (mBluetoothAdapter.isEnabled()) {
+            startScanning();
+        } else {
+            // Turn on bluetooth if it is disabled
+            mBluetoothAdapter.enable();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mLocalManager == null || isUiRestricted()) {
+            return;
+        }
+        stopScanning();
+        removeAllDevices();
+        mLocalManager.setForegroundActivity(null);
+        mLocalManager.getEventManager().unregisterCallback(this);
+    }
+
+    @Override
+    public boolean onPreferenceTreeClick(Preference preference) {
+        if (preference instanceof BluetoothDevicePreference) {
+            stopScanning();
+            BluetoothDevicePreference devicePreference = (BluetoothDevicePreference) preference;
+            mSelectedDevice = devicePreference.getCachedDevice().getDevice();
+            if (mSelectedDevice != null) {
+                mSelectedDeviceList.add(mSelectedDevice);
+            }
+            devicePreference.onClicked();
+            return true;
+        }
+        return super.onPreferenceTreeClick(preference);
+    }
+
+    @Override
+    public void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {
+        removeDevice(cachedDevice);
+    }
+
+    @Override
+    public void onBluetoothStateChanged(int bluetoothState) {
+        switch (bluetoothState) {
+            case BluetoothAdapter.STATE_ON:
+                startScanning();
+                showBluetoothTurnedOnToast();
+                break;
+            case BluetoothAdapter.STATE_OFF:
+                finish();
+                break;
+        }
+    }
+
+    @Override
+    public void onDeviceBondStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
+            int bondState) {
+        if (DEBUG) {
+            Log.d(TAG, "onDeviceBondStateChanged: " + cachedDevice.getName() + ", state = "
+                    + bondState);
+        }
+        if (bondState == BluetoothDevice.BOND_BONDED) {
+            // If one device is connected(bonded), then close this fragment.
+            setResult(RESULT_OK);
+            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.getFeatureFactory().getMetricsFeatureProvider()
+                    .getAttribution(getActivity());
+            final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry(
+                    pageId);
+            HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice);
+        }
+        if (mSelectedDevice != null) {
+            BluetoothDevice device = cachedDevice.getDevice();
+            if (mSelectedDevice.equals(device) && bondState == BluetoothDevice.BOND_NONE) {
+                // If current selected device failed to bond, restart scanning
+                startScanning();
+            }
+        }
+    }
+
+    @Override
+    public void onProfileConnectionStateChanged(@NonNull 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.isConnected()) {
+            final BluetoothDevice device = cachedDevice.getDevice();
+            if (device != null && mSelectedDeviceList.contains(device)) {
+                setResult(RESULT_OK);
+                finish();
+            } else {
+                removeDevice(cachedDevice);
+            }
+        }
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.HEARING_AID_PAIRING;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.hearing_device_pairing_fragment;
+    }
+
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    void addDevice(CachedBluetoothDevice cachedDevice) {
+        if (mBluetoothAdapter == null) {
+            return;
+        }
+        // Do not create new preference while the list shows one of the state messages
+        if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) {
+            return;
+        }
+        if (mDevicePreferenceMap.get(cachedDevice) != null) {
+            return;
+        }
+        String key = cachedDevice.getDevice().getAddress();
+        BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key);
+        if (preference == null) {
+            preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice,
+                    mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO);
+            preference.setKey(key);
+            preference.hideSecondTarget(true);
+        }
+        if (mAvailableHearingDeviceGroup != null) {
+            mAvailableHearingDeviceGroup.addPreference(preference);
+        }
+        mDevicePreferenceMap.put(cachedDevice, preference);
+        if (DEBUG) {
+            Log.d(TAG, "Add device. device: " + cachedDevice);
+        }
+    }
+
+    void removeDevice(CachedBluetoothDevice cachedDevice) {
+        if (DEBUG) {
+            Log.d(TAG, "removeDevice: " + cachedDevice);
+        }
+        BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice);
+        if (mAvailableHearingDeviceGroup != null && preference != null) {
+            mAvailableHearingDeviceGroup.removePreference(preference);
+        }
+    }
+
+    void startScanning() {
+        if (mCachedDeviceManager != null) {
+            mCachedDeviceManager.clearNonBondedDevices();
+        }
+        removeAllDevices();
+        startLeScanning();
+    }
+
+    void stopScanning() {
+        stopLeScanning();
+    }
+
+    private final ScanCallback mLeScanCallback = new ScanCallback() {
+        @Override
+        public void onScanResult(int callbackType, ScanResult result) {
+            handleLeScanResult(result);
+        }
+
+        @Override
+        public void onBatchScanResults(List<ScanResult> results) {
+            for (ScanResult result: results) {
+                handleLeScanResult(result);
+            }
+        }
+
+        @Override
+        public void onScanFailed(int errorCode) {
+            Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+        }
+    };
+
+    void handleLeScanResult(ScanResult result) {
+        if (mCachedDeviceManager == null) {
+            return;
+        }
+        final BluetoothDevice device = result.getDevice();
+        CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
+        if (cachedDevice == null) {
+            cachedDevice = mCachedDeviceManager.addDevice(device);
+        }
+        if (cachedDevice.getHearingAidInfo() == null) {
+            if (DEBUG) {
+                Log.d(TAG, "Set hearing aid info on device: " + cachedDevice);
+            }
+            cachedDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
+        }
+        addDevice(cachedDevice);
+    }
+
+    void startLeScanning() {
+        if (mBluetoothAdapter == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "startLeScanning");
+        }
+        final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner();
+        if (leScanner == null) {
+            Log.w(TAG, "LE scanner not found, cannot start LE scanning");
+        } else {
+            final ScanSettings settings = new ScanSettings.Builder()
+                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+                    .setLegacy(false)
+                    .build();
+            leScanner.startScan(mLeScanFilters, settings, mLeScanCallback);
+            if (mAvailableHearingDeviceGroup != null) {
+                mAvailableHearingDeviceGroup.setProgress(true);
+            }
+        }
+    }
+
+    void stopLeScanning() {
+        if (mBluetoothAdapter == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "stopLeScanning");
+        }
+        final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner();
+        if (leScanner != null) {
+            leScanner.stopScan(mLeScanCallback);
+            if (mAvailableHearingDeviceGroup != null) {
+                mAvailableHearingDeviceGroup.setProgress(false);
+            }
+        }
+    }
+
+    private void removeAllDevices() {
+        mDevicePreferenceMap.clear();
+        if (mAvailableHearingDeviceGroup != null) {
+            mAvailableHearingDeviceGroup.removeAll();
+        }
+    }
+
+    void initPreferencesFromPreferenceScreen() {
+        mAvailableHearingDeviceGroup = findPreference(KEY_AVAILABLE_HEARING_DEVICES);
+    }
+
+    private void initHearingDeviceLeScanFilters() {
+        mLeScanFilters = new ArrayList<>();
+        // Filters for ASHA hearing aids
+        mLeScanFilters.add(
+                new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HEARING_AID).build());
+        mLeScanFilters.add(new ScanFilter.Builder()
+                .setServiceData(BluetoothUuid.HEARING_AID, new byte[0]).build());
+        // Filters for LE audio hearing aids
+        mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build());
+        mLeScanFilters.add(new ScanFilter.Builder()
+                .setServiceData(BluetoothUuid.HAS, new byte[0]).build());
+    }
+
+    void showBluetoothTurnedOnToast() {
+        Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
+                Toast.LENGTH_SHORT).show();
+    }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
index 98d78f2..ac0c63b 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
@@ -156,7 +156,7 @@
         return R.layout.preference_widget_gear;
     }
 
-    CachedBluetoothDevice getCachedDevice() {
+    public CachedBluetoothDevice getCachedDevice() {
         return mCachedDevice;
     }
 
@@ -362,7 +362,11 @@
         }
     }
 
-    void onClicked() {
+    /**
+     * Performs different actions according to the device connected and bonded state after
+     * clicking on the preference.
+     */
+    public void onClicked() {
         Context context = getContext();
         int bondState = mCachedDevice.getBondState();
 
diff --git a/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java b/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java
index 12cbd58..3a16e3e 100644
--- a/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java
+++ b/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java
@@ -29,8 +29,10 @@
 
 import com.android.settings.R;
 import com.android.settings.accessibility.HearingDevicePairingDetail;
+import com.android.settings.accessibility.HearingDevicePairingFragment;
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.flags.Flags;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.HearingAidInfo;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -123,8 +125,11 @@
         final int launchPage = getArguments().getInt(KEY_LAUNCH_PAGE);
         final boolean launchFromA11y = (launchPage == SettingsEnums.ACCESSIBILITY)
                 || (launchPage == SettingsEnums.ACCESSIBILITY_HEARING_AID_SETTINGS);
+        final String a11yDestination = Flags.newHearingDevicePairingPage()
+                ? HearingDevicePairingFragment.class.getName()
+                : HearingDevicePairingDetail.class.getName();
         final String destination = launchFromA11y
-                ? HearingDevicePairingDetail.class.getName()
+                ? a11yDestination
                 : BluetoothPairingDetail.class.getName();
         new SubSettingLauncher(getActivity())
                 .setDestination(destination)
diff --git a/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java b/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java
index d2bc319..ef44843 100644
--- a/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java
@@ -15,18 +15,25 @@
  */
 package com.android.settings.connecteddevice;
 
+import static com.android.settings.accessibility.AccessibilityHearingAidsFragment.KEY_HEARING_DEVICE_ADD_BT_DEVICES;
+
 import android.bluetooth.BluetoothAdapter;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.text.TextUtils;
 
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
+import com.android.settings.accessibility.HearingDevicePairingDetail;
+import com.android.settings.accessibility.HearingDevicePairingFragment;
 import com.android.settings.core.BasePreferenceController;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.flags.Flags;
 import com.android.settingslib.core.lifecycle.LifecycleObserver;
 import com.android.settingslib.core.lifecycle.events.OnStart;
 import com.android.settingslib.core.lifecycle.events.OnStop;
@@ -76,6 +83,21 @@
     }
 
     @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_ADD_BT_DEVICES)) {
+            String destination = Flags.newHearingDevicePairingPage()
+                    ? HearingDevicePairingFragment.class.getName()
+                    : HearingDevicePairingDetail.class.getName();
+            new SubSettingLauncher(preference.getContext())
+                    .setDestination(destination)
+                    .setSourceMetricsCategory(getMetricsCategory())
+                    .launch();
+            return true;
+        }
+        return super.handlePreferenceTreeClick(preference);
+    }
+
+    @Override
     public int getAvailabilityStatus() {
         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
                 ? AVAILABLE
diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java
index 6c1de59..bd57e9d 100644
--- a/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java
@@ -32,6 +32,10 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.appcompat.app.AlertDialog;
 import androidx.fragment.app.FragmentActivity;
@@ -43,6 +47,7 @@
 import com.android.settings.bluetooth.BluetoothPairingDetail;
 import com.android.settings.bluetooth.HearingAidPairingDialogFragment;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.flags.Flags;
 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -77,6 +82,9 @@
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
     private static final int TEST_LAUNCH_PAGE = SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
 
@@ -129,7 +137,22 @@
     }
 
     @Test
-    public void dialogPositiveButtonClick_intentToA11yPairingPage() {
+    @RequiresFlagsEnabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE)
+    public void dialogPositiveButtonClick_intentToNewA11yPairingPage() {
+        setupDialog(SettingsEnums.ACCESSIBILITY);
+        final AlertDialog dialog = (AlertDialog) mFragment.onCreateDialog(Bundle.EMPTY);
+        dialog.show();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+        final Intent intent = shadowOf(mActivity).getNextStartedActivity();
+        assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(HearingDevicePairingFragment.class.getName());
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE)
+    public void dialogPositiveButtonClick_intentToOldA11yPairingPage() {
         setupDialog(SettingsEnums.ACCESSIBILITY);
         final AlertDialog dialog = (AlertDialog) mFragment.onCreateDialog(Bundle.EMPTY);
         dialog.show();
diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java
new file mode 100644
index 0000000..134f865
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.accessibility;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.ScanResult;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.Pair;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.BluetoothDevicePreference;
+import com.android.settings.bluetooth.BluetoothProgressCategory;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.HearingAidInfo;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link HearingDevicePairingFragment}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
+public class HearingDevicePairingFragmentTest {
+
+    private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Spy
+    private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+    @Spy
+    private final HearingDevicePairingFragment mFragment = new TestHearingDevicePairingFragment();
+
+    @Mock
+    private LocalBluetoothManager mLocalManager;
+    @Mock
+    private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock
+    private CachedBluetoothDevice mCachedDevice;
+    @Mock
+    private BluetoothProgressCategory mAvailableHearingDeviceGroup;
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private BluetoothDevice mDevice;
+    private BluetoothDevicePreference mDevicePreference;
+
+
+    @Before
+    public void setUp() {
+        mFragment.mLocalManager = mLocalManager;
+        mFragment.mCachedDeviceManager = mCachedDeviceManager;
+        mFragment.mBluetoothAdapter = mBluetoothAdapter;
+        doReturn(mContext).when(mFragment).getContext();
+        doReturn(mAvailableHearingDeviceGroup).when(mFragment).findPreference(
+                "available_hearing_devices");
+        mFragment.initPreferencesFromPreferenceScreen();
+
+
+        mDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS);
+        doReturn(mDevice).when(mCachedDevice).getDevice();
+        final Pair<Drawable, String> pair = new Pair<>(mock(Drawable.class), "test_device");
+        doReturn(pair).when(mCachedDevice).getDrawableWithDescription();
+
+        mDevicePreference = new BluetoothDevicePreference(mContext, mCachedDevice, true,
+                BluetoothDevicePreference.SortType.TYPE_DEFAULT);
+    }
+
+    @Test
+    public void startAndStopScanning_stateIsCorrect() {
+        mFragment.startScanning();
+
+        verify(mFragment).startLeScanning();
+
+        mFragment.stopScanning();
+
+        verify(mFragment).stopLeScanning();
+    }
+
+    @Test
+    public void onDeviceDeleted_stateIsCorrect() {
+        mFragment.mDevicePreferenceMap.put(mCachedDevice, mDevicePreference);
+
+        assertThat(mFragment.mDevicePreferenceMap).isNotEmpty();
+
+        mFragment.onDeviceDeleted(mCachedDevice);
+
+        assertThat(mFragment.mDevicePreferenceMap).isEmpty();
+        verify(mAvailableHearingDeviceGroup).removePreference(mDevicePreference);
+    }
+
+    @Test
+    public void addDevice_bluetoothOff_doNothing() {
+        doReturn(BluetoothAdapter.STATE_OFF).when(mBluetoothAdapter).getState();
+
+        assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
+
+        mFragment.addDevice(mCachedDevice);
+
+        verify(mAvailableHearingDeviceGroup, never()).addPreference(mDevicePreference);
+        assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void addDevice_addToAvailableHearingDeviceGroup() {
+        doReturn(BluetoothAdapter.STATE_ON).when(mBluetoothAdapter).getState();
+
+        assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0);
+
+        mFragment.addDevice(mCachedDevice);
+
+        verify(mAvailableHearingDeviceGroup).addPreference(mDevicePreference);
+        assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(1);
+    }
+
+    @Test
+    public void handleLeScanResult_markDeviceAsHearingAid() {
+        ScanResult scanResult = mock(ScanResult.class);
+        doReturn(mDevice).when(scanResult).getDevice();
+        doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
+
+        mFragment.handleLeScanResult(scanResult);
+
+        verify(mCachedDevice).setHearingAidInfo(new HearingAidInfo.Builder().build());
+        verify(mFragment).addDevice(mCachedDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_deviceConnected_inSelectedList_finish() {
+        doReturn(true).when(mCachedDevice).isConnected();
+        mFragment.mSelectedDeviceList.add(mDevice);
+
+        mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.A2DP);
+
+        verify(mFragment).finish();
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_deviceConnected_notInSelectedList_deleteDevice() {
+        doReturn(true).when(mCachedDevice).isConnected();
+
+        mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.A2DP);
+
+        verify(mFragment).removeDevice(mCachedDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_deviceNotConnected_doNothing() {
+        doReturn(false).when(mCachedDevice).isConnected();
+
+        mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.A2DP);
+
+        verify(mFragment, never()).finish();
+        verify(mFragment, never()).removeDevice(mCachedDevice);
+    }
+
+    @Test
+    public void onBluetoothStateChanged_stateOn_startScanningAndShowToast() {
+        mFragment.onBluetoothStateChanged(BluetoothAdapter.STATE_ON);
+
+        verify(mFragment).startScanning();
+        verify(mFragment).showBluetoothTurnedOnToast();
+    }
+
+    @Test
+    public void onBluetoothStateChanged_stateOff_finish() {
+        mFragment.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
+
+        verify(mFragment).finish();
+    }
+
+    @Test
+    public void onDeviceBondStateChanged_bonded_finish() {
+        mFragment.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_BONDED);
+
+        verify(mFragment).finish();
+    }
+
+    @Test
+    public void onDeviceBondStateChanged_selectedDeviceNotBonded_startScanning() {
+        mFragment.mSelectedDevice = mDevice;
+
+        mFragment.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_NONE);
+
+        verify(mFragment).startScanning();
+    }
+
+    private class TestHearingDevicePairingFragment extends HearingDevicePairingFragment {
+        @Override
+        protected Preference getCachedPreference(String key) {
+            if (key.equals(TEST_DEVICE_ADDRESS)) {
+                return mDevicePreference;
+            }
+            return super.getCachedPreference(key);
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java
index 7384d3a..63fa88d 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java
@@ -15,34 +15,49 @@
  */
 package com.android.settings.connecteddevice;
 
+import static com.android.settings.accessibility.AccessibilityHearingAidsFragment.KEY_HEARING_DEVICE_ADD_BT_DEVICES;
 import static com.android.settings.core.BasePreferenceController.AVAILABLE;
 import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.bluetooth.BluetoothAdapter;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.TextUtils;
 
 import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.accessibility.HearingDevicePairingDetail;
+import com.android.settings.accessibility.HearingDevicePairingFragment;
+import com.android.settings.flags.Flags;
 import com.android.settingslib.RestrictedPreference;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
 import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowApplicationPackageManager;
 import org.robolectric.util.ReflectionHelpers;
@@ -51,12 +66,16 @@
 @Config(shadows = ShadowApplicationPackageManager.class)
 public class AddDevicePreferenceControllerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock
     private PreferenceScreen mScreen;
     @Mock
     private BluetoothAdapter mBluetoothAdapter;
 
-    private Context mContext;
+    @Spy
+    private Context mContext = ApplicationProvider.getApplicationContext();
     private AddDevicePreferenceController mAddDevicePreferenceController;
     private RestrictedPreference mAddDevicePreference;
     private ShadowApplicationPackageManager mPackageManager;
@@ -66,8 +85,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        mContext = RuntimeEnvironment.application;
-        mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf(
+        mPackageManager = (ShadowApplicationPackageManager) shadowOf(
                 mContext.getPackageManager());
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true);
 
@@ -82,6 +100,8 @@
         when(mBluetoothAdapter.isEnabled()).thenReturn(true);
         when(mScreen.findPreference(key)).thenReturn(mAddDevicePreference);
         mAddDevicePreferenceController.displayPreference(mScreen);
+
+        doNothing().when(mContext).startActivity(any(Intent.class));
     }
 
     @Test
@@ -137,4 +157,30 @@
         assertThat(mAddDevicePreferenceController.getAvailabilityStatus())
                 .isEqualTo(UNSUPPORTED_ON_DEVICE);
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE)
+    public void handlePreferenceClick_A11yPreference_redirectToNewPairingPage() {
+        mAddDevicePreference.setKey(KEY_HEARING_DEVICE_ADD_BT_DEVICES);
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        mAddDevicePreferenceController.handlePreferenceTreeClick(mAddDevicePreference);
+
+        verify(mContext).startActivity(intentCaptor.capture());
+        assertThat(intentCaptor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(HearingDevicePairingFragment.class.getName());
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE)
+    public void handlePreferenceClick_A11yPreference_redirectToOldPairingPage() {
+        mAddDevicePreference.setKey(KEY_HEARING_DEVICE_ADD_BT_DEVICES);
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        mAddDevicePreferenceController.handlePreferenceTreeClick(mAddDevicePreference);
+
+        verify(mContext).startActivity(intentCaptor.capture());
+        assertThat(intentCaptor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(HearingDevicePairingDetail.class.getName());
+    }
 }