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

Commit 2bf73103 authored by jasonwshsu's avatar jasonwshsu
Browse files

Implement Hearing Devices Quick Settings Tile (4/n)

This patch provides
* Show the ordered hearing device list
* Handle hearing device item click callback

Bug: 291423171
Bug: 319197158
Test: atest com.android.settingslib.bluetoth.BluetoothUtilsTest
Test: atest HearingDevicesDialogDelegateTest HearingDevicesListAdapterTest
Flag: ACONFIG com.android.systemui.hearing_aids_qs_tile_dialog DEVELOPMENT
Change-Id: Ic6b72055f73a7f3eafb6f6ec44fc7ea74dc7e2b3
parent 430f6999
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -541,6 +541,25 @@ public class BluetoothUtils {
        return isFilterMatched;
    }

    /**
     * Checks if the Bluetooth device is an available hearing device, which means:
     * 1) currently connected
     * 2) is Hearing Aid
     * 3) connected profile match hearing aid related profiles (e.g. ASHA, HAP)
     *
     * @param cachedDevice the CachedBluetoothDevice
     * @return if the device is Available hearing device
     */
    @WorkerThread
    public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) {
        if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) {
            Log.d(TAG, "isFilterMatched() device : "
                    + cachedDevice.getName() + ", the profile is connected.");
            return true;
        }
        return false;
    }

    /**
     * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means:
     * 1) currently connected
+10 −0
Original line number Diff line number Diff line
@@ -430,4 +430,14 @@ public class BluetoothUtilsTest {
        assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
                mBluetoothDevice)).isEqualTo(true);
    }

    @Test
    public void isAvailableHearingDevice_isConnectedHearingAid_returnTure() {
        when(mCachedBluetoothDevice.isConnectedHearingAidDevice()).thenReturn(true);
        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
        when(mBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
        when(mBluetoothDevice.isConnected()).thenReturn(true);

        assertThat(BluetoothUtils.isAvailableHearingDevice(mCachedBluetoothDevice)).isEqualTo(true);
    }
}
+9 −1
Original line number Diff line number Diff line
@@ -22,6 +22,14 @@
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/device_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/pair_new_device_button" />

    <Button
        android:id="@+id/pair_new_device_button"
        style="@style/BluetoothTileDialog.Device"
@@ -33,7 +41,7 @@
        android:contentDescription="@string/accessibility_hearing_device_pair_new_device"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintTop_toBottomOf="@id/device_list"
        android:drawableStart="@drawable/ic_add"
        android:drawablePadding="20dp"
        android:drawableTint="?android:attr/textColorPrimary"
+144 −2
Original line number Diff line number Diff line
@@ -19,18 +19,38 @@ package com.android.systemui.accessibility.hearingaid;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;

import static java.util.Collections.emptyList;

import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.Visibility;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.tiles.dialog.bluetooth.ActiveHearingDeviceItemFactory;
import com.android.systemui.qs.tiles.dialog.bluetooth.AvailableHearingDeviceItemFactory;
import com.android.systemui.qs.tiles.dialog.bluetooth.ConnectedDeviceItemFactory;
import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItem;
import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItemFactory;
import com.android.systemui.qs.tiles.dialog.bluetooth.SavedHearingDeviceItemFactory;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;

@@ -38,18 +58,44 @@ import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Dialog for showing hearing devices controls.
 */
public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate {
public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
        HearingDeviceItemCallback, BluetoothCallback {

    @VisibleForTesting
    static final String ACTION_BLUETOOTH_DEVICE_DETAILS =
            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS";
    private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";
    private static final String KEY_BLUETOOTH_ADDRESS = "device_address";
    private final SystemUIDialog.Factory mSystemUIDialogFactory;
    private final DialogTransitionAnimator mDialogTransitionAnimator;
    private final ActivityStarter mActivityStarter;
    private final boolean mShowPairNewDevice;
    private final LocalBluetoothManager mLocalBluetoothManager;
    private final Handler mMainHandler;
    private final AudioManager mAudioManager;

    private HearingDevicesListAdapter mDeviceListAdapter;
    private SystemUIDialog mDialog;
    private RecyclerView mDeviceList;
    private Button mPairButton;
    private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of(
            new ActiveHearingDeviceItemFactory(),
            new AvailableHearingDeviceItemFactory(),
            // TODO(b/331305850): setHearingAidInfo() for connected but not connect to profile
            // hearing device only called from
            // settings/bluetooth/DeviceListPreferenceFragment#handleLeScanResult, so we don't know
            // it is connected but not yet connect to profile hearing device in systemui.
            // Show all connected but not connect to profile bluetooth device for now.
            new ConnectedDeviceItemFactory(),
            new SavedHearingDeviceItemFactory()
    );

    /** Factory to create a {@link HearingDevicesDialogDelegate} dialog instance. */
    @AssistedFactory
@@ -64,11 +110,17 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate {
            @Assisted boolean showPairNewDevice,
            SystemUIDialog.Factory systemUIDialogFactory,
            ActivityStarter activityStarter,
            DialogTransitionAnimator dialogTransitionAnimator) {
            DialogTransitionAnimator dialogTransitionAnimator,
            @Nullable LocalBluetoothManager localBluetoothManager,
            @Main Handler handler,
            AudioManager audioManager) {
        mShowPairNewDevice = showPairNewDevice;
        mSystemUIDialogFactory = systemUIDialogFactory;
        mActivityStarter = activityStarter;
        mDialogTransitionAnimator = dialogTransitionAnimator;
        mLocalBluetoothManager = localBluetoothManager;
        mMainHandler = handler;
        mAudioManager = audioManager;
    }

    @Override
@@ -80,6 +132,47 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate {
        return dialog;
    }

    @Override
    public void onDeviceItemGearClicked(@NonNull  DeviceItem deviceItem, @NonNull View view) {
        dismissDialogIfExists();
        Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS);
        Bundle bundle = new Bundle();
        bundle.putString(KEY_BLUETOOTH_ADDRESS, deviceItem.getCachedBluetoothDevice().getAddress());
        intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
                mDialogTransitionAnimator.createActivityTransitionController(view));
    }

    @Override
    public void onDeviceItemOnClicked(@NonNull  DeviceItem deviceItem, @NonNull View view) {
        CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice();
        switch (deviceItem.getType()) {
            case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE ->
                    cachedBluetoothDevice.disconnect();
            case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> cachedBluetoothDevice.setActive();
            case SAVED_BLUETOOTH_DEVICE -> cachedBluetoothDevice.connect();
        }
    }

    @Override
    public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice,
            int bluetoothProfile) {
        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
    }

    @Override
    public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
            int state, int bluetoothProfile) {
        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
    }

    @Override
    public void onAclConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
            int state) {
        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
    }

    @Override
    public void beforeCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) {
        dialog.setTitle(R.string.quick_settings_hearing_devices_dialog_title);
@@ -95,10 +188,34 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate {
    @Override
    public void onCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) {
        mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
        mDeviceList = dialog.requireViewById(R.id.device_list);

        setupDeviceListView(dialog);
        setupPairNewDeviceButton(dialog, mShowPairNewDevice ? VISIBLE : GONE);
    }

    @Override
    public void onStart(@NonNull SystemUIDialog dialog) {
        if (mLocalBluetoothManager == null) {
            return;
        }
        mLocalBluetoothManager.getEventManager().registerCallback(this);
    }

    @Override
    public void onStop(@NonNull SystemUIDialog dialog) {
        if (mLocalBluetoothManager == null) {
            return;
        }
        mLocalBluetoothManager.getEventManager().unregisterCallback(this);
    }

    private void setupDeviceListView(SystemUIDialog dialog) {
        mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
        mDeviceListAdapter = new HearingDevicesListAdapter(getHearingDevicesList(), this);
        mDeviceList.setAdapter(mDeviceListAdapter);
    }

    private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) {
        if (visibility == VISIBLE) {
            mPairButton.setOnClickListener(v -> {
@@ -113,6 +230,31 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate {
        }
    }

    private List<DeviceItem> getHearingDevicesList() {
        if (mLocalBluetoothManager == null
                || !mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
            return emptyList();
        }

        return mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream()
                .map(this::createHearingDeviceItem)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private DeviceItem createHearingDeviceItem(CachedBluetoothDevice cachedDevice) {
        final Context context = mDialog.getContext();
        if (cachedDevice == null) {
            return null;
        }
        for (DeviceItemFactory itemFactory : mHearingDeviceItemFactoryList) {
            if (itemFactory.isFilterMatched(context, cachedDevice, mAudioManager)) {
                return itemFactory.create(context, cachedDevice);
            }
        }
        return null;
    }

    private void dismissDialogIfExists() {
        if (mDialog != null) {
            mDialog.dismiss();
+137 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.accessibility.hearingaid;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItem;
import com.android.systemui.res.R;

import kotlin.Pair;

import java.util.List;

/**
 * Adapter for showing hearing device item list {@link DeviceItem}.
 */
public class HearingDevicesListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final List<DeviceItem> mItemList;
    private final HearingDeviceItemCallback mCallback;

    public HearingDevicesListAdapter(List<DeviceItem> itemList,
            HearingDeviceItemCallback callback) {
        mItemList = itemList;
        mCallback = callback;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(
                R.layout.bluetooth_device_item, viewGroup, false);
        return new DeviceItemViewHolder(view, viewGroup.getContext());
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
        DeviceItem item = mItemList.get(position);
        ((DeviceItemViewHolder) viewHolder).bindView(item, mCallback);
    }

    @Override
    public int getItemCount() {
        return mItemList.size();
    }

    /**
     * Updates items in the adapter.
     *
     * @param itemList bluetooth device item list
     */
    public void refreshDeviceItemList(List<DeviceItem> itemList) {
        mItemList.clear();
        mItemList.addAll(itemList);
        notifyDataSetChanged();
    }

    /**
     * Interface to provide callbacks when click on the device item in hearing device quick
     * settings tile.
     */
    public interface HearingDeviceItemCallback {
        /**
         * Called when gear view in device item is clicked.
         *
         * @param deviceItem bluetooth device item
         * @param view       the view that was clicked
         */
        void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view);

        /**
         * Called when device item is clicked.
         *
         * @param deviceItem bluetooth device item
         * @param view       the view that was clicked
         */
        void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view);
    }

    private static class DeviceItemViewHolder extends RecyclerView.ViewHolder {
        private final Context mContext;
        private final View mContainer;
        private final TextView mNameView;
        private final TextView mSummaryView;
        private final ImageView mIconView;
        private final View mGearView;

        DeviceItemViewHolder(@NonNull View itemView, Context context) {
            super(itemView);
            mContext = context;
            mContainer = itemView.requireViewById(R.id.bluetooth_device_row);
            mNameView = itemView.requireViewById(R.id.bluetooth_device_name);
            mSummaryView = itemView.requireViewById(R.id.bluetooth_device_summary);
            mIconView = itemView.requireViewById(R.id.bluetooth_device_icon);
            mGearView = itemView.requireViewById(R.id.gear_icon);
        }

        public void bindView(DeviceItem item, HearingDeviceItemCallback callback) {
            mContainer.setEnabled(item.isEnabled());
            mContainer.setOnClickListener(view -> callback.onDeviceItemOnClicked(item, view));
            Integer backgroundResId = item.getBackground();
            if (backgroundResId != null) {
                mContainer.setBackground(mContext.getDrawable(item.getBackground()));
            }
            mNameView.setText(item.getDeviceName());
            mSummaryView.setText(item.getConnectionSummary());
            Pair<Drawable, String> iconPair = item.getIconWithDescription();
            if (iconPair != null) {
                mIconView.setImageDrawable(iconPair.getFirst());
                mIconView.setContentDescription(iconPair.getSecond());
            }
            mGearView.setOnClickListener(view -> callback.onDeviceItemGearClicked(item, view));
        }
    }
}
Loading