diff --git a/res/values/strings.xml b/res/values/strings.xml index c0a89c3f907..6cb6ea5eae6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6784,6 +6784,8 @@ + + diff --git a/res/xml/fast_pair_devices.xml b/res/xml/fast_pair_devices.xml new file mode 100644 index 00000000000..f549ff46e49 --- /dev/null +++ b/res/xml/fast_pair_devices.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceDashboardFragment.java new file mode 100644 index 00000000000..2c6c1121a71 --- /dev/null +++ b/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceDashboardFragment.java @@ -0,0 +1,55 @@ +/* + * 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 android.app.settings.SettingsEnums; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +/** This fragment contains list of available FastPair device */ +@SearchIndexable(forTarget = SearchIndexable.MOBILE) +public class FastPairDeviceDashboardFragment extends DashboardFragment { + + private static final String TAG = "FastPairDeviceFrag"; + + @Override + public int getHelpResource() { + return R.string.help_url_connected_devices_fast_pair_devices; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.fast_pair_devices; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FAST_PAIR_DEVICES; + } + + /** For Search. */ + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.fast_pair_devices); +} diff --git a/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java b/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java new file mode 100644 index 00000000000..ac117f1f342 --- /dev/null +++ b/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupController.java @@ -0,0 +1,143 @@ +/* + * 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 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 androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.flags.Flags; +import com.android.settings.overlay.FeatureFactory; + +/** + * Controller to maintain the {@link PreferenceGroup} for all Fast Pair devices. It uses {@link + * DevicePreferenceCallback} to add/remove {@link Preference} + */ +public class FastPairDeviceGroupController extends BasePreferenceController + implements PreferenceControllerMixin, DefaultLifecycleObserver, DevicePreferenceCallback { + + private static final String KEY = "fast_pair_device_list"; + + @VisibleForTesting PreferenceGroup mPreferenceGroup; + private final FastPairDeviceUpdater mFastPairDeviceUpdater; + private final BluetoothAdapter mBluetoothAdapter; + @VisibleForTesting IntentFilter mIntentFilter; + + @VisibleForTesting + BroadcastReceiver mReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updatePreferenceVisibility(); + } + }; + + public FastPairDeviceGroupController(Context context) { + super(context, KEY); + if (Flags.enableSubsequentPairSettingsIntegration()) { + FastPairFeatureProvider fastPairFeatureProvider = + FeatureFactory.getFeatureFactory().getFastPairFeatureProvider(); + mFastPairDeviceUpdater = + fastPairFeatureProvider.getFastPairDeviceUpdater(context, this); + } else { + mFastPairDeviceUpdater = null; + } + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + } + + @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) { + mPreferenceGroup = screen.findPreference(KEY); + mPreferenceGroup.setVisible(false); + + 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 String getPreferenceKey() { + return KEY; + } + + @Override + public void onDeviceAdded(Preference preference) { + if (preference == null) return; + mPreferenceGroup.addPreference(preference); + updatePreferenceVisibility(); + } + + @Override + public void onDeviceRemoved(Preference preference) { + if (preference == null) return; + mPreferenceGroup.removePreference(preference); + updatePreferenceVisibility(); + } + + private void updatePreferenceVisibility() { + mPreferenceGroup.setVisible( + mBluetoothAdapter != null + && mBluetoothAdapter.isEnabled() + && mPreferenceGroup.getPreferenceCount() > 0); + } + + @VisibleForTesting + public void setPreferenceGroup(PreferenceGroup preferenceGroup) { + mPreferenceGroup = preferenceGroup; + } +} diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp index f620348326d..910bbbd2abb 100644 --- a/tests/robotests/Android.bp +++ b/tests/robotests/Android.bp @@ -58,6 +58,9 @@ android_robolectric_test { "androidx.test.ext.junit", "androidx.test.rules", "androidx.test.runner", + "flag-junit", + "aconfig_settings_flags_lib", + "platform-test-annotations", ], libs: [ diff --git a/tests/robotests/AndroidManifest.xml b/tests/robotests/AndroidManifest.xml index e0050ef9e04..22fce4f1749 100644 --- a/tests/robotests/AndroidManifest.xml +++ b/tests/robotests/AndroidManifest.xml @@ -1,7 +1,25 @@ + + + + diff --git a/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java new file mode 100644 index 00000000000..bf40bd24341 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/fastpair/FastPairDeviceGroupControllerTest.java @@ -0,0 +1,157 @@ +/* + * 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 android.bluetooth.BluetoothAdapter; +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 androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; + +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.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 FastPairDeviceGroupControllerTest { + + @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; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private Context mContext; + private FastPairDeviceGroupController mFastPairDeviceGroupController; + private PreferenceGroup mPreferenceGroup; + 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()); + mFastPairDeviceGroupController = new FastPairDeviceGroupController(mContext); + mPreferenceGroup = spy(new PreferenceCategory(mContext)); + mPreferenceGroup.setVisible(false); + mFastPairDeviceGroupController.setPreferenceGroup(mPreferenceGroup); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void testRegister() { + // register the callback in onStart() + mFastPairDeviceGroupController.onStart(mLifecycleOwner); + verify(mFastPairDeviceUpdater).registerCallback(); + verify(mContext) + .registerReceiver( + mFastPairDeviceGroupController.mReceiver, + mFastPairDeviceGroupController.mIntentFilter, + Context.RECEIVER_EXPORTED); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void testUnregister() { + // register it first + mContext.registerReceiver( + mFastPairDeviceGroupController.mReceiver, null, Context.RECEIVER_EXPORTED); + + // unregister the callback in onStop() + mFastPairDeviceGroupController.onStop(mLifecycleOwner); + verify(mFastPairDeviceUpdater).unregisterCallback(); + verify(mContext).unregisterReceiver(mFastPairDeviceGroupController.mReceiver); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void testGetAvailabilityStatus_noFastPairFeature_returnUnSupported() { + doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); + + assertThat(mFastPairDeviceGroupController.getAvailabilityStatus()) + .isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void testGetAvailabilityStatus_noBluetoothFeature_returnUnSupported() { + doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); + + assertThat(mFastPairDeviceGroupController.getAvailabilityStatus()) + .isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void testGetAvailabilityStatus_withBluetoothFastPairFeature_returnSupported() { + doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); + + assertThat(mFastPairDeviceGroupController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION) + public void testUpdatePreferenceVisibility_bluetoothIsDisable_shouldHidePreference() { + mShadowBluetoothAdapter.setEnabled(false); + Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.sendBroadcast(intent); + + assertThat(mPreferenceGroup.isVisible()).isFalse(); + } +}