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

Commit a60f80f4 authored by Jason Hsu's avatar Jason Hsu Committed by Android (Google) Code Review
Browse files

Merge "Connected devices page did not show correct summary when member device connect" into main

parents e880a15a 15c6533e
Loading
Loading
Loading
Loading
+9 −9
Original line number Diff line number Diff line
@@ -89,7 +89,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
    private LocalBluetoothManager mManager;
    private LocalBluetoothProfileManager mProfileManager;
    private CachedBluetoothDevice mCachedDevice;
    private List<CachedBluetoothDevice> mAllOfCachedDevices;
    private Set<CachedBluetoothDevice> mCachedDeviceGroup;
    private Map<String, List<CachedBluetoothDevice>> mProfileDeviceMap =
            new HashMap<String, List<CachedBluetoothDevice>>();
    private boolean mIsLeContactSharingEnabled = false;
@@ -105,7 +105,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
        mManager = manager;
        mProfileManager = mManager.getProfileManager();
        mCachedDevice = device;
        mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
        mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
    }

    @Override
@@ -310,10 +310,10 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
    private List<LocalBluetoothProfile> getProfiles() {
        List<LocalBluetoothProfile> result = new ArrayList<>();
        mProfileDeviceMap.clear();
        if (mAllOfCachedDevices == null || mAllOfCachedDevices.isEmpty()) {
        if (mCachedDeviceGroup == null || mCachedDeviceGroup.isEmpty()) {
            return result;
        }
        for (CachedBluetoothDevice cachedItem : mAllOfCachedDevices) {
        for (CachedBluetoothDevice cachedItem : mCachedDeviceGroup) {
            List<LocalBluetoothProfile> tmpResult = cachedItem.getUiAccessibleProfiles();
            for (LocalBluetoothProfile profile : tmpResult) {
                if (mProfileDeviceMap.containsKey(profile.toString())) {
@@ -514,7 +514,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll

    @Override
    public void onPause() {
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.unregisterCallback(this);
        }
        mProfileManager.removeServiceListener(this);
@@ -523,7 +523,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
    @Override
    public void onResume() {
        updateLeAudioConfig();
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.registerCallback(this);
        }
        mProfileManager.addServiceListener(this);
@@ -545,11 +545,11 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll

    @Override
    public void onDeviceAttributesChanged() {
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.unregisterCallback(this);
        }
        mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.registerCallback(this);
        }

+37 −13
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.GearPreference;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;

@@ -55,6 +56,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * BluetoothDevicePreference is the preference type used to display each remote
@@ -76,7 +78,10 @@ public final class BluetoothDevicePreference extends GearPreference {
    }

    private final CachedBluetoothDevice mCachedDevice;
    private Set<CachedBluetoothDevice> mCachedDeviceGroup;

    private final UserManager mUserManager;
    private final LocalBluetoothManager mLocalBtManager;

    private Set<BluetoothDevice> mBluetoothDevices;
    @VisibleForTesting
@@ -113,6 +118,21 @@ public final class BluetoothDevicePreference extends GearPreference {
        @Override
        public void onDeviceAttributesChanged() {
            onPreferenceAttributesChanged();
            Set<CachedBluetoothDevice> newCachedDeviceGroup = new HashSet<>(
                    Utils.findAllCachedBluetoothDevicesByGroupId(mLocalBtManager, mCachedDevice));
            if (!mCachedDeviceGroup.equals(newCachedDeviceGroup)) {
                for (CachedBluetoothDevice cachedBluetoothDevice : mCachedDeviceGroup) {
                    cachedBluetoothDevice.unregisterCallback(this);
                }
                unregisterMetadataChangedListener();

                mCachedDeviceGroup = newCachedDeviceGroup;

                for (CachedBluetoothDevice cachedBluetoothDevice : mCachedDeviceGroup) {
                    cachedBluetoothDevice.registerCallback(getContext().getMainExecutor(), this);
                }
                registerMetadataChangedListener();
            }
        }
    }

@@ -121,6 +141,7 @@ public final class BluetoothDevicePreference extends GearPreference {
        super(context, null);
        mResources = getContext().getResources();
        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
        mLocalBtManager = Utils.getLocalBluetoothManager(context);
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mShowDevicesWithoutNames = showDeviceWithoutNames;

@@ -131,6 +152,8 @@ public final class BluetoothDevicePreference extends GearPreference {
        }

        mCachedDevice = cachedDevice;
        mCachedDeviceGroup = new HashSet<>(
                Utils.findAllCachedBluetoothDevicesByGroupId(mLocalBtManager, mCachedDevice));
        mCallback = new BluetoothDevicePreferenceCallback();
        mId = sNextId.getAndIncrement();
        mType = type;
@@ -164,7 +187,9 @@ public final class BluetoothDevicePreference extends GearPreference {
    protected void onPrepareForRemoval() {
        super.onPrepareForRemoval();
        if (!mIsCallbackRemoved) {
            mCachedDevice.unregisterCallback(mCallback);
            for (CachedBluetoothDevice cachedBluetoothDevice : mCachedDeviceGroup) {
                cachedBluetoothDevice.unregisterCallback(mCallback);
            }
            unregisterMetadataChangedListener();
            mIsCallbackRemoved = true;
        }
@@ -178,7 +203,9 @@ public final class BluetoothDevicePreference extends GearPreference {
    public void onAttached() {
        super.onAttached();
        if (mIsCallbackRemoved) {
            mCachedDevice.registerCallback(mCallback);
            for (CachedBluetoothDevice cachedBluetoothDevice : mCachedDeviceGroup) {
                cachedBluetoothDevice.registerCallback(getContext().getMainExecutor(), mCallback);
            }
            registerMetadataChangedListener();
            mIsCallbackRemoved = false;
        }
@@ -189,7 +216,9 @@ public final class BluetoothDevicePreference extends GearPreference {
    public void onDetached() {
        super.onDetached();
        if (!mIsCallbackRemoved) {
            mCachedDevice.unregisterCallback(mCallback);
            for (CachedBluetoothDevice cachedBluetoothDevice : mCachedDeviceGroup) {
                cachedBluetoothDevice.unregisterCallback(mCallback);
            }
            unregisterMetadataChangedListener();
            mIsCallbackRemoved = true;
        }
@@ -200,16 +229,11 @@ public final class BluetoothDevicePreference extends GearPreference {
            Log.d(TAG, "No mBluetoothAdapter");
            return;
        }
        if (mBluetoothDevices == null) {
            mBluetoothDevices = new HashSet<>();
        }
        mBluetoothDevices.clear();
        if (mCachedDevice.getDevice() != null) {
            mBluetoothDevices.add(mCachedDevice.getDevice());
        }
        for (CachedBluetoothDevice cbd : mCachedDevice.getMemberDevice()) {
            mBluetoothDevices.add(cbd.getDevice());
        }

        mBluetoothDevices = mCachedDeviceGroup.stream()
                .map(CachedBluetoothDevice::getDevice)
                .collect(Collectors.toCollection(HashSet::new));

        if (mBluetoothDevices.isEmpty()) {
            Log.d(TAG, "No BT device to register.");
            return;
+11 −11
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.widget.LayoutPreference;

import java.util.List;
import java.util.Set;

/**
 * This class adds a header with device name and status (connected/disconnected, etc.).
@@ -90,7 +90,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
    LayoutPreference mLayoutPreference;
    LocalBluetoothManager mManager;
    private CachedBluetoothDevice mCachedDevice;
    private List<CachedBluetoothDevice> mAllOfCachedDevices;
    private Set<CachedBluetoothDevice> mCachedDeviceGroup;
    @VisibleForTesting
    Handler mHandler = new Handler(Looper.getMainLooper());
    @VisibleForTesting
@@ -128,7 +128,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
            return;
        }
        mIsRegisterCallback = true;
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.registerCallback(this);
        }
        refresh();
@@ -139,7 +139,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
        if (!mIsRegisterCallback) {
            return;
        }
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.unregisterCallback(this);
        }

@@ -155,7 +155,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
        mCachedDevice = cachedBluetoothDevice;
        mManager = bluetoothManager;
        mProfileManager = bluetoothManager.getProfileManager();
        mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
        mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
    }

    @VisibleForTesting
@@ -230,7 +230,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
        // Init the battery layouts.
        hideAllOfBatteryLayouts();
        LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
        if (mAllOfCachedDevices.isEmpty()) {
        if (mCachedDeviceGroup.isEmpty()) {
            Log.e(TAG, "There is no LeAudioProfile.");
            return;
        }
@@ -244,7 +244,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
            return;
        }

        for (CachedBluetoothDevice cachedDevice : mAllOfCachedDevices) {
        for (CachedBluetoothDevice cachedDevice : mCachedDeviceGroup) {
            int deviceId = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
            Log.d(TAG, "LeAudioDevices:" + cachedDevice.getDevice().getAnonymizedAddress()
                    + ", deviceId:" + deviceId);
@@ -300,15 +300,15 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr

    @Override
    public void onDeviceAttributesChanged() {
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.unregisterCallback(this);
        }
        mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
        for (CachedBluetoothDevice item : mAllOfCachedDevices) {
        mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.registerCallback(this);
        }

        if (!mAllOfCachedDevices.isEmpty()) {
        if (!mCachedDeviceGroup.isEmpty()) {
            refresh();
        }
    }
+9 −10
Original line number Diff line number Diff line
@@ -48,8 +48,9 @@ import com.android.settingslib.utils.ThreadUtils;

import com.google.common.base.Supplier;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

@@ -239,12 +240,12 @@ public final class Utils {
     * @param cachedBluetoothDevice The main cachedBluetoothDevice.
     * @return all cachedBluetoothDevices with the same groupId.
     */
    public static List<CachedBluetoothDevice> getAllOfCachedBluetoothDevices(
    public static Set<CachedBluetoothDevice> findAllCachedBluetoothDevicesByGroupId(
            LocalBluetoothManager localBtMgr,
            CachedBluetoothDevice cachedBluetoothDevice) {
        List<CachedBluetoothDevice> cachedBluetoothDevices = new ArrayList<>();
        Set<CachedBluetoothDevice> cachedBluetoothDevices = new HashSet<>();
        if (cachedBluetoothDevice == null) {
            Log.e(TAG, "getAllOfCachedBluetoothDevices: no cachedBluetoothDevice");
            Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: no cachedBluetoothDevice");
            return cachedBluetoothDevices;
        }
        int deviceGroupId = cachedBluetoothDevice.getGroupId();
@@ -254,7 +255,7 @@ public final class Utils {
        }

        if (localBtMgr == null) {
            Log.e(TAG, "getAllOfCachedBluetoothDevices: no LocalBluetoothManager");
            Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: no LocalBluetoothManager");
            return cachedBluetoothDevices;
        }
        CachedBluetoothDevice mainDevice =
@@ -262,16 +263,14 @@ public final class Utils {
                        .filter(cachedDevice -> cachedDevice.getGroupId() == deviceGroupId)
                        .findFirst().orElse(null);
        if (mainDevice == null) {
            Log.e(TAG, "getAllOfCachedBluetoothDevices: groupId = " + deviceGroupId
            Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: groupId = " + deviceGroupId
                    + ", no main device.");
            return cachedBluetoothDevices;
        }
        cachedBluetoothDevice = mainDevice;
        cachedBluetoothDevices.add(cachedBluetoothDevice);
        for (CachedBluetoothDevice member : cachedBluetoothDevice.getMemberDevice()) {
            cachedBluetoothDevices.add(member);
        }
        Log.d(TAG, "getAllOfCachedBluetoothDevices: groupId = " + deviceGroupId
        cachedBluetoothDevices.addAll(cachedBluetoothDevice.getMemberDevice());
        Log.d(TAG, "findAllCachedBluetoothDevicesByGroupId: groupId = " + deviceGroupId
                + " , cachedBluetoothDevice = " + cachedBluetoothDevice
                + " , deviceList = " + cachedBluetoothDevices);
        return cachedBluetoothDevices;
+138 −31
Original line number Diff line number Diff line
@@ -18,10 +18,10 @@ package com.android.settings.bluetooth;
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,22 +32,31 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.UserManager;
import android.util.Pair;
import android.view.ContextThemeWrapper;

import androidx.test.core.app.ApplicationProvider;

import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

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.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;

@@ -57,18 +66,21 @@ import java.util.Comparator;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowAlertDialogCompat.class})
@Config(shadows = {ShadowAlertDialogCompat.class,
        com.android.settings.testutils.shadow.ShadowBluetoothUtils.class})
public class BluetoothDevicePreferenceTest {
    private static final boolean SHOW_DEVICES_WITHOUT_NAMES = true;
    private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
    private static final String MAC_ADDRESS_2 = "05:52:C7:0B:D8:3C";
    private static final String MAC_ADDRESS_3 = "06:52:C7:0B:D8:3C";
    private static final String MAC_ADDRESS_4 = "07:52:C7:0B:D8:3C";
    private static final String TEST_MAC_ADDRESS = "04:52:C7:0B:D8:3C";
    private static final String TEST_MAC_ADDRESS_1 = "05:52:C7:0B:D8:3C";
    private static final String TEST_MAC_ADDRESS_2 = "06:52:C7:0B:D8:3C";
    private static final String TEST_MAC_ADDRESS_3 = "07:52:C7:0B:D8:3C";
    private static final Comparator<BluetoothDevicePreference> COMPARATOR =
            Comparator.naturalOrder();
    private static final String FAKE_DESCRIPTION = "fake_description";
    private static final int TEST_DEVICE_GROUP_ID = 1;

    private Context mContext;
    @Rule
    public final MockitoRule mockito = MockitoJUnit.rule();
    @Mock
    private CachedBluetoothDevice mCachedBluetoothDevice;
    @Mock
@@ -89,35 +101,37 @@ public class BluetoothDevicePreferenceTest {
    private Drawable mDrawable;
    @Mock
    private BluetoothAdapter mBluetoothAdapter;
    @Mock
    private LocalBluetoothManager mLocalBluetoothManager;
    @Mock
    private CachedBluetoothDeviceManager mDeviceManager;

    private Context mContext = ApplicationProvider.getApplicationContext();
    private FakeFeatureFactory mFakeFeatureFactory;
    private MetricsFeatureProvider mMetricsFeatureProvider;

    private BluetoothDevicePreference mPreference;
    private List<BluetoothDevicePreference> mPreferenceList = new ArrayList<>();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        Context context = spy(RuntimeEnvironment.application.getApplicationContext());
        mContext = new ContextThemeWrapper(context, R.style.Theme_Settings);
        mContext.setTheme(R.style.Theme_Settings);
        mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
        mMetricsFeatureProvider = mFakeFeatureFactory.getMetricsFeatureProvider();
        when(mCachedBluetoothDevice.getAddress()).thenReturn(MAC_ADDRESS);
        when(mCachedBluetoothDevice.getDrawableWithDescription())
                .thenReturn(new Pair<>(mDrawable, FAKE_DESCRIPTION));
        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
        when(mCachedDevice1.getAddress()).thenReturn(MAC_ADDRESS_2);
        when(mCachedDevice1.getDrawableWithDescription())
                .thenReturn(new Pair<>(mDrawable, FAKE_DESCRIPTION));
        when(mCachedDevice1.getDevice()).thenReturn(mBluetoothDevice1);
        when(mCachedDevice2.getAddress()).thenReturn(MAC_ADDRESS_3);
        when(mCachedDevice2.getDrawableWithDescription())
                .thenReturn(new Pair<>(mDrawable, FAKE_DESCRIPTION));
        when(mCachedDevice2.getDevice()).thenReturn(mBluetoothDevice2);
        when(mCachedDevice3.getAddress()).thenReturn(MAC_ADDRESS_4);
        when(mCachedDevice3.getDrawableWithDescription())
                .thenReturn(new Pair<>(mDrawable, FAKE_DESCRIPTION));
        when(mCachedDevice3.getDevice()).thenReturn(mBluetoothDevice3);
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
        when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
        prepareCachedBluetoothDevice(mCachedBluetoothDevice, TEST_MAC_ADDRESS,
                new Pair<>(mDrawable, FAKE_DESCRIPTION), TEST_DEVICE_GROUP_ID, mBluetoothDevice);
        prepareCachedBluetoothDevice(mCachedDevice1, TEST_MAC_ADDRESS_1,
                new Pair<>(mDrawable, FAKE_DESCRIPTION), TEST_DEVICE_GROUP_ID, mBluetoothDevice1);
        prepareCachedBluetoothDevice(mCachedDevice2, TEST_MAC_ADDRESS_2,
                new Pair<>(mDrawable, FAKE_DESCRIPTION), TEST_DEVICE_GROUP_ID, mBluetoothDevice2);
        prepareCachedBluetoothDevice(mCachedDevice3, TEST_MAC_ADDRESS_3,
                new Pair<>(mDrawable, FAKE_DESCRIPTION), TEST_DEVICE_GROUP_ID, mBluetoothDevice3);
        when(mDeviceManager.getCachedDevicesCopy()).thenReturn(
                ImmutableList.of(mCachedBluetoothDevice));

        mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
                SHOW_DEVICES_WITHOUT_NAMES, BluetoothDevicePreference.SortType.TYPE_DEFAULT);
        mPreference.mBluetoothAdapter = mBluetoothAdapter;
@@ -301,7 +315,8 @@ public class BluetoothDevicePreferenceTest {
        // callback is not removed.
        mPreference.onAttached();

        verify(mCachedBluetoothDevice, times(1)).registerCallback(any());
        verify(mCachedBluetoothDevice, times(1)).registerCallback(eq(mContext.getMainExecutor()),
                any());
        verify(mBluetoothAdapter, times(1)).addOnMetadataChangedListener(any(), any(), any());
    }

@@ -313,7 +328,99 @@ public class BluetoothDevicePreferenceTest {
        mPreference.onAttached();

        verify(mCachedBluetoothDevice, times(1)).unregisterCallback(any());
        verify(mCachedBluetoothDevice, times(2)).registerCallback(any());
        verify(mCachedBluetoothDevice, times(2)).registerCallback(eq(mContext.getMainExecutor()),
                any());
        verify(mBluetoothAdapter, times(2)).addOnMetadataChangedListener(any(), any(), any());
    }

    @Test
    public void onDeviceAttributesChanged_updatePreference() {
        when(mCachedBluetoothDevice.getName()).thenReturn("Name");
        mPreference.onAttached();
        final String updatedName = "updatedName";
        when(mCachedBluetoothDevice.getName()).thenReturn(updatedName);

        getCachedBluetoothDeviceCallback().onDeviceAttributesChanged();

        assertThat(mPreference.getTitle().toString()).isEqualTo(updatedName);
    }

    @Test
    public void onAttached_memberDevicesAdded_registerAllCallback() {
        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(
                ImmutableSet.of(mCachedDevice1, mCachedDevice2, mCachedDevice3));
        when(mDeviceManager.getCachedDevicesCopy()).thenReturn(
                ImmutableList.of(mCachedBluetoothDevice, mCachedDevice1, mCachedDevice2,
                        mCachedDevice3));
        mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
                SHOW_DEVICES_WITHOUT_NAMES, BluetoothDevicePreference.SortType.TYPE_DEFAULT);

        mPreference.onAttached();

        verify(mCachedBluetoothDevice).registerCallback(eq(mContext.getMainExecutor()), any());
        verify(mCachedDevice1).registerCallback(eq(mContext.getMainExecutor()), any());
        verify(mCachedDevice2).registerCallback(eq(mContext.getMainExecutor()), any());
        verify(mCachedDevice3).registerCallback(eq(mContext.getMainExecutor()), any());
    }

    @Test
    public void onDetached_memberDevicesAdded_unregisterAllCallback() {
        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(
                ImmutableSet.of(mCachedDevice1, mCachedDevice2, mCachedDevice3));
        when(mDeviceManager.getCachedDevicesCopy()).thenReturn(
                ImmutableList.of(mCachedBluetoothDevice, mCachedDevice1, mCachedDevice2,
                        mCachedDevice3));
        mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
                SHOW_DEVICES_WITHOUT_NAMES, BluetoothDevicePreference.SortType.TYPE_DEFAULT);

        mPreference.onAttached();
        mPreference.onDetached();

        verify(mCachedBluetoothDevice).unregisterCallback(any());
        verify(mCachedDevice1).unregisterCallback(any());
        verify(mCachedDevice2).unregisterCallback(any());
        verify(mCachedDevice3).unregisterCallback(any());
    }

    @Test
    public void onDeviceAttributesChanged_memberDevicesChanged_registerOnlyExistDeviceCallback() {
        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(
                ImmutableSet.of(mCachedDevice1, mCachedDevice2, mCachedDevice3));
        when(mDeviceManager.getCachedDevicesCopy()).thenReturn(
                ImmutableList.of(mCachedBluetoothDevice, mCachedDevice1, mCachedDevice2,
                        mCachedDevice3));
        mPreference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice,
                SHOW_DEVICES_WITHOUT_NAMES, BluetoothDevicePreference.SortType.TYPE_DEFAULT);
        mPreference.onAttached();
        when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(
                ImmutableSet.of(mCachedDevice1, mCachedDevice2));
        when(mDeviceManager.getCachedDevicesCopy()).thenReturn(
                ImmutableList.of(mCachedBluetoothDevice, mCachedDevice1, mCachedDevice2));

        getCachedBluetoothDeviceCallback().onDeviceAttributesChanged();

        verify(mCachedBluetoothDevice, times(2)).registerCallback(eq(mContext.getMainExecutor()),
                any());
        verify(mCachedDevice1, times(2)).registerCallback(eq(mContext.getMainExecutor()), any());
        verify(mCachedDevice2, times(2)).registerCallback(eq(mContext.getMainExecutor()), any());
        verify(mCachedDevice3, times(1)).registerCallback(eq(mContext.getMainExecutor()), any());
    }

    private void prepareCachedBluetoothDevice(CachedBluetoothDevice cachedDevice, String address,
            Pair<Drawable, String> drawableWithDescription, int groupId,
            BluetoothDevice bluetoothDevice) {
        when(cachedDevice.getAddress()).thenReturn(address);
        when(cachedDevice.getDrawableWithDescription()).thenReturn(drawableWithDescription);
        when(cachedDevice.getGroupId()).thenReturn(groupId);
        when(cachedDevice.getDevice()).thenReturn(bluetoothDevice);
    }

    private CachedBluetoothDevice.Callback getCachedBluetoothDeviceCallback() {
        ArgumentCaptor<CachedBluetoothDevice.Callback> callbackCaptor = ArgumentCaptor.forClass(
                CachedBluetoothDevice.Callback.class);
        verify(mCachedBluetoothDevice).registerCallback(eq(mContext.getMainExecutor()),
                callbackCaptor.capture());

        return callbackCaptor.getValue();
    }
}