diff --git a/res/values/strings.xml b/res/values/strings.xml index 1cf5bd76427..539d51b53da 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -255,6 +255,8 @@ See all + + See all Stylus diff --git a/res/xml/connected_devices.xml b/res/xml/connected_devices.xml index a0e0a1f26d6..5d7e0a8a1ed 100644 --- a/res/xml/connected_devices.xml +++ b/res/xml/connected_devices.xml @@ -61,6 +61,22 @@ android:fragment="com.android.settings.connecteddevice.PreviouslyConnectedDeviceDashboardFragment"/> + + + + + mPreferenceList = new ArrayList<>(); + + private PreferenceGroup mPreferenceGroup; + private FastPairDeviceUpdater mFastPairDeviceUpdater; + private BluetoothAdapter mBluetoothAdapter; + + @VisibleForTesting Preference mSeeAllPreference; + @VisibleForTesting IntentFilter mIntentFilter; + + @VisibleForTesting + BroadcastReceiver mReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updatePreferenceVisibility(); + } + }; + + public FastPairDevicePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + + if (Flags.enableSubsequentPairSettingsIntegration()) { + FastPairFeatureProvider fastPairFeatureProvider = + FeatureFactory.getFeatureFactory().getFastPairFeatureProvider(); + mFastPairDeviceUpdater = + fastPairFeatureProvider.getFastPairDeviceUpdater(context, this); + } else { + mFastPairDeviceUpdater = null; + } + mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mFastPairDeviceUpdater != null) { + mFastPairDeviceUpdater.registerCallback(); + } + mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mFastPairDeviceUpdater != null) { + mFastPairDeviceUpdater.unregisterCallback(); + } + mContext.unregisterReceiver(mReceiver); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceGroup = screen.findPreference(getPreferenceKey()); + mSeeAllPreference = mPreferenceGroup.findPreference(KEY_SEE_ALL); + updatePreferenceVisibility(); + + if (isAvailable()) { + final Context context = screen.getContext(); + mFastPairDeviceUpdater.setPreferenceContext(context); + mFastPairDeviceUpdater.forceUpdate(); + } + } + + @Override + public int getAvailabilityStatus() { + return (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + && mFastPairDeviceUpdater != null) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void onDeviceAdded(Preference preference) { + if (preference == null) { + if (DEBUG) { + Log.d(TAG, "onDeviceAdd receives null preference. Ignore."); + } + return; + } + + // Keep showing the latest MAX_DEVICE_NUM devices. + // The preference for the latest device has top preference order. + int idx = Collections.binarySearch(mPreferenceList, preference); + // Binary search returns the index of the search key if it is contained in the list; + // otherwise, (-(insertion point) - 1). + // The insertion point is defined as the point at which the key would be inserted into the + // list: the index of the first element greater than the key, or list.size() if all elements + // in the list are less than the specified key. + if (idx > 0) { + if (DEBUG) { + Log.d(TAG, "onDeviceAdd receives duplicate preference. Ignore."); + } + return; + } + idx = -1 * (idx + 1); + mPreferenceList.add(idx, preference); + if (idx < MAX_DEVICE_NUM) { + if (mPreferenceList.size() > MAX_DEVICE_NUM) { + mPreferenceGroup.removePreference(mPreferenceList.get(MAX_DEVICE_NUM)); + } + mPreferenceGroup.addPreference(preference); + } + updatePreferenceVisibility(); + } + + @Override + public void onDeviceRemoved(Preference preference) { + if (preference == null) { + if (DEBUG) { + Log.d(TAG, "onDeviceRemoved receives null preference. Ignore."); + } + return; + } + + // Keep showing the latest MAX_DEVICE_NUM devices. + // The preference for the latest device has top preference order. + final int idx = mPreferenceList.indexOf(preference); + mPreferenceList.remove(preference); + if (idx < MAX_DEVICE_NUM) { + mPreferenceGroup.removePreference(preference); + if (mPreferenceList.size() >= MAX_DEVICE_NUM) { + mPreferenceGroup.addPreference(mPreferenceList.get(MAX_DEVICE_NUM - 1)); + } + } + updatePreferenceVisibility(); + } + + @VisibleForTesting + void setPreferenceGroup(PreferenceGroup preferenceGroup) { + mPreferenceGroup = preferenceGroup; + } + + @VisibleForTesting + void updatePreferenceVisibility() { + if (mBluetoothAdapter != null + && mBluetoothAdapter.isEnabled() + && mPreferenceList.size() > 0) { + mPreferenceGroup.setVisible(true); + mSeeAllPreference.setVisible(mPreferenceList.size() > MAX_DEVICE_NUM); + } else { + mPreferenceGroup.setVisible(false); + mSeeAllPreference.setVisible(false); + } + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java index bf40bd24341..9241d8bb7dd 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java @@ -25,11 +25,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Looper; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -38,11 +40,13 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.flags.Flags; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.widget.GearPreference; import com.android.settingslib.core.lifecycle.Lifecycle; import org.junit.Before; @@ -69,6 +73,7 @@ public class FastPairDeviceGroupControllerTest { @Mock private DashboardFragment mDashboardFragment; @Mock private FastPairDeviceUpdater mFastPairDeviceUpdater; @Mock private PackageManager mPackageManager; + @Mock private PreferenceManager mPreferenceManager; private ShadowBluetoothAdapter mShadowBluetoothAdapter; private Context mContext; private FastPairDeviceGroupController mFastPairDeviceGroupController; @@ -88,6 +93,7 @@ public class FastPairDeviceGroupControllerTest { doReturn(mFastPairDeviceUpdater).when(provider).getFastPairDeviceUpdater(any(), any()); mFastPairDeviceGroupController = new FastPairDeviceGroupController(mContext); mPreferenceGroup = spy(new PreferenceCategory(mContext)); + doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager(); mPreferenceGroup.setVisible(false); mFastPairDeviceGroupController.setPreferenceGroup(mPreferenceGroup); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); @@ -109,7 +115,7 @@ public class FastPairDeviceGroupControllerTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void testUnregister() { - // register it first + // register broadcast first mContext.registerReceiver( mFastPairDeviceGroupController.mReceiver, null, Context.RECEIVER_EXPORTED); @@ -148,10 +154,21 @@ public class FastPairDeviceGroupControllerTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) public void testUpdatePreferenceVisibility_bluetoothIsDisable_shouldHidePreference() { + mShadowBluetoothAdapter.setEnabled(true); + final GearPreference preference1 = new GearPreference(mContext, null /* AttributeSet */); + mFastPairDeviceGroupController.onDeviceAdded(preference1); + assertThat(mPreferenceGroup.isVisible()).isTrue(); + mShadowBluetoothAdapter.setEnabled(false); + // register broadcast first + mContext.registerReceiver( + mFastPairDeviceGroupController.mReceiver, + mFastPairDeviceGroupController.mIntentFilter, + Context.RECEIVER_EXPORTED_UNAUDITED); Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED); mContext.sendBroadcast(intent); + shadowOf(Looper.getMainLooper()).idle(); assertThat(mPreferenceGroup.isVisible()).isFalse(); } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java new file mode 100644 index 00000000000..8a7fc73561a --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDevicePreferenceControllerTest.java @@ -0,0 +1,260 @@ +/* + * 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.connecteddevice.fastpair; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Looper; +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.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; + +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.flags.Flags; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.widget.GearPreference; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +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.shadow.api.Shadow; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowBluetoothAdapter.class) +public class FastPairDevicePreferenceControllerTest { + + private static final String KEY = "test_key"; + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Mock private DashboardFragment mDashboardFragment; + @Mock private FastPairDeviceUpdater mFastPairDeviceUpdater; + @Mock private PackageManager mPackageManager; + @Mock private PreferenceManager mPreferenceManager; + private Context mContext; + private FastPairDevicePreferenceController mFastPairDevicePrefController; + private PreferenceGroup mPreferenceGroup; + private Preference mSeeAllPreference; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + doReturn(mContext).when(mDashboardFragment).getContext(); + doReturn(mPackageManager).when(mContext).getPackageManager(); + FastPairFeatureProvider provider = + FakeFeatureFactory.setupForTest().getFastPairFeatureProvider(); + doReturn(mFastPairDeviceUpdater).when(provider).getFastPairDeviceUpdater(any(), any()); + mFastPairDevicePrefController = new FastPairDevicePreferenceController(mContext, KEY); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + + mPreferenceGroup = spy(new PreferenceCategory(mContext)); + doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager(); + mSeeAllPreference = spy(new Preference(mContext)); + mPreferenceGroup.setVisible(false); + mSeeAllPreference.setVisible(false); + mFastPairDevicePrefController.setPreferenceGroup(mPreferenceGroup); + mFastPairDevicePrefController.mSeeAllPreference = mSeeAllPreference; + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void onStart_registerCallback() { + // register the callback in onStart() + mFastPairDevicePrefController.onStart(mLifecycleOwner); + verify(mFastPairDeviceUpdater).registerCallback(); + verify(mContext) + .registerReceiver( + mFastPairDevicePrefController.mReceiver, + mFastPairDevicePrefController.mIntentFilter, + Context.RECEIVER_EXPORTED_UNAUDITED); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void onStop_unregisterCallback() { + // register broadcast first + mContext.registerReceiver( + mFastPairDevicePrefController.mReceiver, null, Context.RECEIVER_EXPORTED_UNAUDITED); + + // unregister the callback in onStop() + mFastPairDevicePrefController.onStop(mLifecycleOwner); + verify(mFastPairDeviceUpdater).unregisterCallback(); + verify(mContext).unregisterReceiver(mFastPairDevicePrefController.mReceiver); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void getAvailabilityStatus_noBluetoothFeature_returnUnsupported() { + doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); + + assertThat(mFastPairDevicePrefController.getAvailabilityStatus()) + .isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void getAvailabilityStatus_noFastPairFeature_returnUnsupported() { + doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); + + assertThat(mFastPairDevicePrefController.getAvailabilityStatus()) + .isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void getAvailabilityStatus_bothBluetoothFastPairFeature_returnSupported() { + doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); + + assertThat(mFastPairDevicePrefController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void onDeviceAdded_addThreeFastPairDevicePreference_displayThreeNoSeeAll() { + mShadowBluetoothAdapter.setEnabled(true); + final GearPreference preference1 = new GearPreference(mContext, null /* AttributeSet */); + final GearPreference preference2 = new GearPreference(mContext, null /* AttributeSet */); + final GearPreference preference3 = new GearPreference(mContext, null /* AttributeSet */); + + mFastPairDevicePrefController.onDeviceAdded(preference1); + mFastPairDevicePrefController.onDeviceAdded(preference2); + mFastPairDevicePrefController.onDeviceAdded(preference3); + + assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(3); + assertThat(mPreferenceGroup.isVisible()).isTrue(); + assertThat(mSeeAllPreference.isVisible()).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void onDeviceAdded_addFourDevicePreference_onlyDisplayThreeWithSeeAll() { + mShadowBluetoothAdapter.setEnabled(true); + final GearPreference preference1 = new GearPreference(mContext, null /* AttributeSet */); + preference1.setOrder(4); + final GearPreference preference2 = new GearPreference(mContext, null /* AttributeSet */); + preference2.setOrder(3); + final GearPreference preference3 = new GearPreference(mContext, null /* AttributeSet */); + preference3.setOrder(1); + final GearPreference preference4 = new GearPreference(mContext, null /* AttributeSet */); + preference4.setOrder(2); + final GearPreference preference5 = new GearPreference(mContext, null /* AttributeSet */); + preference5.setOrder(5); + + mFastPairDevicePrefController.onDeviceAdded(preference1); + mFastPairDevicePrefController.onDeviceAdded(preference2); + mFastPairDevicePrefController.onDeviceAdded(preference3); + mFastPairDevicePrefController.onDeviceAdded(preference4); + mFastPairDevicePrefController.onDeviceAdded(preference5); + + // 3 GearPreference and 1 see all preference + assertThat(mPreferenceGroup.getPreference(0)).isEqualTo(preference3); + assertThat(mPreferenceGroup.getPreference(1)).isEqualTo(preference4); + assertThat(mPreferenceGroup.getPreference(2)).isEqualTo(preference2); + assertThat(mPreferenceGroup.isVisible()).isTrue(); + assertThat(mSeeAllPreference.isVisible()).isTrue(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void onDeviceRemoved_removeFourthDevice_hideSeeAll() { + mShadowBluetoothAdapter.setEnabled(true); + final GearPreference preference1 = new GearPreference(mContext, null /* AttributeSet */); + preference1.setOrder(1); + final GearPreference preference2 = new GearPreference(mContext, null /* AttributeSet */); + preference2.setOrder(2); + final GearPreference preference3 = new GearPreference(mContext, null /* AttributeSet */); + preference3.setOrder(3); + final GearPreference preference4 = new GearPreference(mContext, null /* AttributeSet */); + preference4.setOrder(4); + final GearPreference preference5 = new GearPreference(mContext, null /* AttributeSet */); + preference5.setOrder(5); + + mFastPairDevicePrefController.onDeviceAdded(preference1); + mFastPairDevicePrefController.onDeviceAdded(preference2); + mFastPairDevicePrefController.onDeviceAdded(preference3); + mFastPairDevicePrefController.onDeviceAdded(preference4); + mFastPairDevicePrefController.onDeviceAdded(preference5); + + mFastPairDevicePrefController.onDeviceRemoved(preference4); + mFastPairDevicePrefController.onDeviceRemoved(preference2); + + // 3 GearPreference and no see all preference + assertThat(mPreferenceGroup.getPreference(0)).isEqualTo(preference1); + assertThat(mPreferenceGroup.getPreference(1)).isEqualTo(preference3); + assertThat(mPreferenceGroup.getPreference(2)).isEqualTo(preference5); + assertThat(mPreferenceGroup.isVisible()).isTrue(); + assertThat(mSeeAllPreference.isVisible()).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void updatePreferenceVisibility_bluetoothIsDisable_shouldHidePreference() { + mShadowBluetoothAdapter.setEnabled(true); + final GearPreference preference1 = new GearPreference(mContext, null /* AttributeSet */); + mFastPairDevicePrefController.onDeviceAdded(preference1); + assertThat(mPreferenceGroup.isVisible()).isTrue(); + + mShadowBluetoothAdapter.setEnabled(false); + // register broadcast first + mContext.registerReceiver( + mFastPairDevicePrefController.mReceiver, + mFastPairDevicePrefController.mIntentFilter, + Context.RECEIVER_EXPORTED_UNAUDITED); + Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.sendBroadcast(intent); + + shadowOf(Looper.getMainLooper()).idle(); + assertThat(mPreferenceGroup.isVisible()).isFalse(); + } +}