From b3a236d70370a15406a23deb3570bb514fbf077d Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 30 May 2023 11:16:16 +0800 Subject: [PATCH 1/6] Remove primaryUserOnly param from UsageStats UsageStats App List will then has same behavior as other App Lists, can show apps from work profile. Bug: 284902200 Test: Manually on device with Headless System User Mode Change-Id: Ibaf9a01b8d82c342a3aab73bafc9925c4b2ac146 --- src/com/android/settings/spa/development/UsageStats.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/android/settings/spa/development/UsageStats.kt b/src/com/android/settings/spa/development/UsageStats.kt index b681d7579de..4d9c4551f10 100644 --- a/src/com/android/settings/spa/development/UsageStats.kt +++ b/src/com/android/settings/spa/development/UsageStats.kt @@ -32,7 +32,6 @@ object UsageStatsPageProvider : SettingsPageProvider { AppListPage( title = stringResource(R.string.testing_usage_stats), listModel = rememberContext(::UsageStatsListModel), - primaryUserOnly = true, ) } } From c24e305fbf4c5c650dd93c12fa50bb0e23757bf4 Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Wed, 7 Jun 2023 00:02:48 +0000 Subject: [PATCH 2/6] Wait for systemui udfps overlay ready to show settings udfps enroll view. Previously, we show settings's udfps enroll animation view (the fingerprint icon and progress view) once the FingerprintEnrollEnrolling is shown. However, touch events have to wait for systemui's udfps overlay to be valid. This CL lets settings's udfps enroll view wait for systemui's overlay. 1. Sets udfps enroll animation view's default visibility Gone. 2. Propagates FingerprintManager#onUdfpsOverlayShown to FingerprintEnrollEnrolling and when it's called, set the enroll view visible. Besides, this CL renames onPointerDown() and onPointerUp() with Udfps. Bug: 280718879 Test: atest FingerprintEnrollEnrollingTest Change-Id: Ieed3e74c182828918785edcacb021f19a3665f2a --- res/layout/udfps_enroll_view.xml | 3 +- .../biometrics/BiometricEnrollSidecar.java | 48 +++++++++++++------ .../FingerprintEnrollEnrolling.java | 11 ++++- .../fingerprint/FingerprintEnrollSidecar.java | 13 +++-- .../fingerprint/FingerprintUpdater.java | 13 +++-- .../fingerprint/UdfpsEnrollEnrollingView.java | 9 ++-- .../FingerprintEnrollProgressViewModel.java | 4 +- .../FingerprintEnrollEnrollingTest.java | 21 ++++++-- ...ingerprintEnrollProgressViewModelTest.java | 4 +- 9 files changed, 88 insertions(+), 38 deletions(-) diff --git a/res/layout/udfps_enroll_view.xml b/res/layout/udfps_enroll_view.xml index 6bf339b6b07..bd626093ce4 100644 --- a/res/layout/udfps_enroll_view.xml +++ b/res/layout/udfps_enroll_view.xml @@ -18,7 +18,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/udfps_animation_view" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:visibility="gone"> Date: Tue, 13 Jun 2023 17:08:33 +0800 Subject: [PATCH 3/6] Call onViewCreated in DashboardFragment To let fragment's view lifecycle owner passed in to controllers. For UI usage, view lifecycle owner is the best choice, controller could use this lifecycle owner to observe events and do UI related works. Usage DataSaverController as first example. Bug: 287005021 Test: Manually for DataSaverController Change-Id: Id965ea3a24d61f4d0ec6735632944f41f72ba06a --- .../specialaccess/DataSaverController.kt | 2 +- .../specialaccess/SpecialAccessSettings.java | 10 ---------- .../settings/dashboard/DashboardFragment.java | 12 ++++++++++++ .../android/settings/datausage/DataSaverSummary.kt | 7 ------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/com/android/settings/applications/specialaccess/DataSaverController.kt b/src/com/android/settings/applications/specialaccess/DataSaverController.kt index 3a2fdb002b1..baed0aa5ec4 100644 --- a/src/com/android/settings/applications/specialaccess/DataSaverController.kt +++ b/src/com/android/settings/applications/specialaccess/DataSaverController.kt @@ -51,7 +51,7 @@ class DataSaverController(context: Context, key: String) : BasePreferenceControl preference = screen.findPreference(preferenceKey)!! } - fun init(viewLifecycleOwner: LifecycleOwner) { + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { preference.summary = getUnrestrictedSummary(mContext) diff --git a/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java b/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java index 9f4c8958cf4..2cbc30422fc 100644 --- a/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java +++ b/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java @@ -21,10 +21,6 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.MANAGE_DE import android.app.settings.SettingsEnums; import android.os.Bundle; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -50,12 +46,6 @@ public class SpecialAccessSettings extends DashboardFragment { MANAGE_DEVICE_ADMIN_APPS, R.string.manage_device_admin); } - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - use(DataSaverController.class).init(getViewLifecycleOwner()); - } - @Override protected int getPreferenceScreenResId() { return R.xml.special_access; diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index f8a5d76b6fa..d4acfa11c57 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -25,11 +25,14 @@ import android.preference.PreferenceManager.OnActivityResultListener; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; +import android.view.View; import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; @@ -169,6 +172,15 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner(); + for (AbstractPreferenceController controller : mControllers) { + controller.onViewCreated(viewLifecycleOwner); + } + } + @Override public void onCategoriesChanged(Set categories) { final String categoryKey = getCategoryKey(); diff --git a/src/com/android/settings/datausage/DataSaverSummary.kt b/src/com/android/settings/datausage/DataSaverSummary.kt index 13fbbfa3069..0828d362410 100644 --- a/src/com/android/settings/datausage/DataSaverSummary.kt +++ b/src/com/android/settings/datausage/DataSaverSummary.kt @@ -19,11 +19,9 @@ import android.app.settings.SettingsEnums import android.content.Context import android.os.Bundle import android.telephony.SubscriptionManager -import android.view.View import android.widget.Switch import com.android.settings.R import com.android.settings.SettingsActivity -import com.android.settings.applications.specialaccess.DataSaverController import com.android.settings.dashboard.DashboardFragment import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.widget.SettingsMainSwitchBar @@ -59,11 +57,6 @@ class DataSaverSummary : DashboardFragment() { } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - use(DataSaverController::class.java).init(viewLifecycleOwner) - } - override fun onResume() { super.onResume() dataSaverBackend.addListener(dataSaverBackendListener) From b7a4a7daa412084c051064c22b872b4d1c32bc37 Mon Sep 17 00:00:00 2001 From: Rhed Jao Date: Mon, 17 Apr 2023 11:39:40 +0000 Subject: [PATCH 4/6] [Settings] Add a verification flow for exiting repair mode Handles the ACTION_CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL intent to launch the confirm device credential activity for users to exiting repair mode. The activity passes a special user id USER_REPAIR_MODE to the framework and verify credentials that the user enrolled in normal mode. Bug: 277561275 Test: am start -a android.app.action.PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL settings put global repair_mode_active 1 am start -a android.app.action.CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL The credential is verified successfully. Change-Id: I9ffe32f9925ee2b990c49d5674d27196a4c9edf7 --- AndroidManifest.xml | 1 + res/values/strings.xml | 12 ++++++++++++ src/com/android/settings/Utils.java | 18 ++++++++++++++++-- .../password/ChooseLockSettingsHelper.java | 3 ++- .../ConfirmDeviceCredentialActivity.java | 16 ++++++++++++++-- .../ConfirmDeviceCredentialBaseFragment.java | 4 +++- .../settings/password/ConfirmLockPassword.java | 10 ++++++++++ .../settings/password/ConfirmLockPattern.java | 12 ++++++++++-- 8 files changed, 68 insertions(+), 8 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 185d21ec7b2..9c5d6c6a404 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2398,6 +2398,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index bd4e0186b3c..a78e74a2828 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3495,6 +3495,18 @@ Also use password to unlock this device + + Verify pattern + + Verify PIN + + Verify password + + Enter your device pattern enrolled in normal mode to continue + + Enter your device PIN enrolled in normal mode to continue + + Enter your device password enrolled in normal mode to continue diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 68b1a48a380..a2195df1b6b 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -708,9 +708,13 @@ public final class Utils extends com.android.settingslib.Utils { final int userId = bundle.getInt(Intent.EXTRA_USER_ID, UserHandle.myUserId()); if (userId == LockPatternUtils.USER_FRP) { return allowAnyUser ? userId : checkUserOwnsFrpCredential(context, userId); - } else { - return allowAnyUser ? userId : enforceSameOwner(context, userId); } + if (userId == LockPatternUtils.USER_REPAIR_MODE) { + enforceRepairModeActive(context); + // any users can exit repair mode + return userId; + } + return allowAnyUser ? userId : enforceSameOwner(context, userId); } /** @@ -729,6 +733,16 @@ public final class Utils extends com.android.settingslib.Utils { + " does not own frp credential."); } + /** + * Throws {@link SecurityException} if repair mode is not active on the device. + */ + private static void enforceRepairModeActive(Context context) { + if (LockPatternUtils.isRepairModeActive(context)) { + return; + } + throw new SecurityException("Repair mode is not active on the device."); + } + /** * Returns the given user id if it belongs to the current user. * diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java index 943a937a32a..9533314c8a2 100644 --- a/src/com/android/settings/password/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java @@ -362,7 +362,8 @@ public final class ChooseLockSettingsHelper { } @NonNull public ChooseLockSettingsHelper build() { - if (!mAllowAnyUserId && mUserId != LockPatternUtils.USER_FRP) { + if (!mAllowAnyUserId && mUserId != LockPatternUtils.USER_FRP + && mUserId != LockPatternUtils.USER_REPAIR_MODE) { Utils.enforceSameOwner(mActivity, mUserId); } diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index d409c0f9dfb..314ce053127 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -166,8 +166,12 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { mDetails = intent.getCharSequenceExtra(KeyguardManager.EXTRA_DESCRIPTION); String alternateButton = intent.getStringExtra( KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); - boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction()); - boolean remoteValidation = + final boolean frp = + KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction()); + final boolean repairMode = + KeyguardManager.ACTION_CONFIRM_REPAIR_MODE_DEVICE_CREDENTIAL + .equals(intent.getAction()); + final boolean remoteValidation = KeyguardManager.ACTION_CONFIRM_REMOTE_DEVICE_CREDENTIAL.equals(intent.getAction()); mTaskOverlay = isInternalActivity() && intent.getBooleanExtra(KeyguardManager.EXTRA_FORCE_TASK_OVERLAY, false); @@ -222,6 +226,14 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { .setExternal(true) .setUserId(LockPatternUtils.USER_FRP) .show(); + } else if (repairMode) { + final ChooseLockSettingsHelper.Builder builder = + new ChooseLockSettingsHelper.Builder(this); + launchedCDC = builder.setHeader(mTitle) + .setDescription(mDetails) + .setExternal(true) + .setUserId(LockPatternUtils.USER_REPAIR_MODE) + .show(); } else if (remoteValidation) { RemoteLockscreenValidationSession remoteLockscreenValidationSession = intent.getParcelableExtra( diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java index 5a123b89315..43d8440512b 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java @@ -106,6 +106,7 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr protected boolean mFrp; protected boolean mRemoteValidation; protected boolean mRequestWriteRepairModePassword; + protected boolean mRepairMode; protected CharSequence mAlternateButtonText; protected BiometricManager mBiometricManager; @Nullable protected RemoteLockscreenValidationSession mRemoteLockscreenValidationSession; @@ -181,6 +182,7 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(), isInternalActivity()); mFrp = (mUserId == LockPatternUtils.USER_FRP); + mRepairMode = (mUserId == LockPatternUtils.USER_REPAIR_MODE); mUserManager = UserManager.get(getActivity()); mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId); mLockPatternUtils = new LockPatternUtils(getActivity()); @@ -269,7 +271,7 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr // verifyTiedProfileChallenge. In such case, we also wanna show the user message that // fingerprint is disabled due to device restart. protected boolean isStrongAuthRequired() { - return mFrp + return mFrp || mRepairMode || !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId) || !mUserManager.isUserUnlocked(mUserId); } diff --git a/src/com/android/settings/password/ConfirmLockPassword.java b/src/com/android/settings/password/ConfirmLockPassword.java index 1b535069f3c..c6022b5d3ce 100644 --- a/src/com/android/settings/password/ConfirmLockPassword.java +++ b/src/com/android/settings/password/ConfirmLockPassword.java @@ -284,6 +284,11 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { return mIsAlpha ? getString(R.string.lockpassword_confirm_your_password_header_frp) : getString(R.string.lockpassword_confirm_your_pin_header_frp); } + if (mRepairMode) { + return mIsAlpha + ? getString(R.string.lockpassword_confirm_repair_mode_password_header) + : getString(R.string.lockpassword_confirm_repair_mode_pin_header); + } if (mRemoteValidation) { return getString(R.string.lockpassword_remote_validation_header); } @@ -307,6 +312,11 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { return mIsAlpha ? getString(R.string.lockpassword_confirm_your_password_details_frp) : getString(R.string.lockpassword_confirm_your_pin_details_frp); } + if (mRepairMode) { + return mIsAlpha + ? getString(R.string.lockpassword_confirm_repair_mode_password_details) + : getString(R.string.lockpassword_confirm_repair_mode_pin_details); + } if (mRemoteValidation) { return getContext().getString(mIsAlpha ? R.string.lockpassword_remote_validation_password_details diff --git a/src/com/android/settings/password/ConfirmLockPattern.java b/src/com/android/settings/password/ConfirmLockPattern.java index 3951bde9bac..a2bcb5af510 100644 --- a/src/com/android/settings/password/ConfirmLockPattern.java +++ b/src/com/android/settings/password/ConfirmLockPattern.java @@ -179,7 +179,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { // ability to disable the pattern in L. Remove this block after // ensuring it's safe to do so. (Note that ConfirmLockPassword // doesn't have this). - if (!mFrp && !mRemoteValidation + if (!mFrp && !mRemoteValidation && !mRepairMode && !mLockPatternUtils.isLockPatternEnabled(mEffectiveUserId)) { getActivity().setResult(Activity.RESULT_OK); getActivity().finish(); @@ -308,6 +308,9 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { if (mFrp) { return getString(R.string.lockpassword_confirm_your_pattern_details_frp); } + if (mRepairMode) { + return getString(R.string.lockpassword_confirm_repair_mode_pattern_details); + } if (mRemoteValidation) { return getString( R.string.lockpassword_remote_validation_pattern_details); @@ -402,7 +405,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } private String getDefaultHeader() { - if (mFrp) return getString(R.string.lockpassword_confirm_your_pattern_header_frp); + if (mFrp) { + return getString(R.string.lockpassword_confirm_your_pattern_header_frp); + } + if (mRepairMode) { + return getString(R.string.lockpassword_confirm_repair_mode_pattern_header); + } if (mRemoteValidation) { return getString(R.string.lockpassword_remote_validation_header); } From 36dfb5ac0b181c7580492d45c7dc4f6136490645 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 15 Jun 2023 16:33:05 +0800 Subject: [PATCH 5/6] Fix SettingsRoboTests Mock the getViewLifecycleOwner in test to fix. Fix: 287387514 Test: m RunSettingsRoboTests Change-Id: I346930e9655e4f78227276aee4c2a4e5ae6962e0 --- .../accessibility/AccessibilitySettingsForSetupWizardTest.java | 2 ++ .../TextReadingPreferenceFragmentForSetupWizardTest.java | 3 +++ ...creenMagnificationPreferenceFragmentForSetupWizardTest.java | 3 +++ ...ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java | 3 +++ ...oggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java | 3 +++ 5 files changed, 14 insertions(+) diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java index e14e27109e0..ea2852fd75e 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardTest.java @@ -38,6 +38,7 @@ import android.content.pm.ServiceInfo; import android.view.accessibility.AccessibilityManager; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; @@ -93,6 +94,7 @@ public class AccessibilitySettingsForSetupWizardTest { when(mAccessibilityManager.getInstalledAccessibilityServiceList()).thenReturn( mAccessibilityServices); doReturn(mActivity).when(mFragment).getActivity(); + doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner(); doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class); } diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java index 1cd301f98b9..4ee2a2dedbd 100644 --- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java @@ -22,6 +22,7 @@ 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.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -29,6 +30,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; @@ -73,6 +75,7 @@ public class TextReadingPreferenceFragmentForSetupWizardTest { final LayoutPreference resetPreference = new LayoutPreference(mContext, R.layout.accessibility_text_reading_reset_button); doReturn(mContext).when(mFragment).getContext(); + doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner(); doReturn(resetPreference).when(mFragment).findPreference(RESET_KEY); doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class); } diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java index 84783b21a3a..aa622f58afd 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java @@ -20,6 +20,7 @@ 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.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,6 +28,7 @@ import static org.mockito.Mockito.when; import android.app.settings.SettingsEnums; import android.content.Context; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; @@ -75,6 +77,7 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest { mFragment = spy(new TestToggleScreenMagnificationPreferenceFragmentForSetupWizard(mContext)); doReturn(mActivity).when(mFragment).getActivity(); + doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner(); when(mActivity.getSwitchBar()).thenReturn(mSwitchBar); doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class); } diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java index c604652fd50..77e5b1f9b9d 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizardTest.java @@ -20,6 +20,7 @@ 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.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,6 +29,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; import androidx.test.core.app.ApplicationProvider; @@ -72,6 +74,7 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizardTest { public void setUp() { mFragment = spy(new TestToggleScreenReaderPreferenceFragmentForSetupWizard(mContext)); doReturn(mActivity).when(mFragment).getActivity(); + doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner(); when(mActivity.getSwitchBar()).thenReturn(mSwitchBar); doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class); } diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java index 78938310d9a..8878064afa0 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest.java @@ -20,6 +20,7 @@ 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.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,6 +29,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; import androidx.test.core.app.ApplicationProvider; @@ -72,6 +74,7 @@ public class ToggleSelectToSpeakPreferenceFragmentForSetupWizardTest { public void setUp() { mFragment = spy(new TestToggleSelectToSpeakPreferenceFragmentForSetupWizard(mContext)); doReturn(mActivity).when(mFragment).getActivity(); + doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner(); when(mActivity.getSwitchBar()).thenReturn(mSwitchBar); doReturn(mFooterBarMixin).when(mGlifLayoutView).getMixin(FooterBarMixin.class); } From bb47f32011b037ba23323ea9733a0c14516894bd Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 13 Jun 2023 20:30:26 +0800 Subject: [PATCH 6/6] Fix bluetooth settings pairing page stuck There is heavy work to do when add device to list in the DeviceListPreferenceFragment, off load these work from main thread to solve the issue. Make devicePreferenceMap a ConcurrentHashMap to avoid potential race condition. Also no longer use getCachedPreference(key) since we not put anything into the cache, the fallback flow is always used. Also in BluetoothDevicePreference.onPreferenceAttributesChanged(), move more heavy work to background thread. Using System.currentTimeMillis() to sort devices could cause flaky because System.currentTimeMillis() could be same for different device, use AtomicInteger instead. Fix: 286628533 Test: Following the step in bug Change-Id: Ia9750adb6b4c1424d084381e9d7c2ca8e7912391 --- .../BluetoothDevicePairingDetailBase.java | 12 +- .../bluetooth/BluetoothDevicePreference.java | 64 ++-- .../DeviceListPreferenceFragment.java | 351 ------------------ .../bluetooth/DeviceListPreferenceFragment.kt | 348 +++++++++++++++++ .../BluetoothDevicePairingDetailBaseTest.java | 6 +- .../bluetooth/BluetoothPairingDetailTest.java | 23 +- 6 files changed, 412 insertions(+), 392 deletions(-) delete mode 100644 src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java create mode 100644 src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index 7ee61ee249d..f2bc6fcfde6 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -128,7 +128,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere if (device != null && mSelectedList.contains(device)) { setResult(RESULT_OK); finish(); - } else if (mDevicePreferenceMap.containsKey(cachedDevice)) { + } else { onDeviceDeleted(cachedDevice); } } @@ -175,8 +175,6 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void updateContent(int bluetoothState) { switch (bluetoothState) { case BluetoothAdapter.STATE_ON: - mDevicePreferenceMap.clear(); - clearPreferenceGroupCache(); mBluetoothAdapter.enable(); enableScanning(); break; @@ -187,14 +185,6 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } } - /** - * Clears all cached preferences in {@code preferenceGroup}. - */ - private void clearPreferenceGroupCache() { - cacheRemoveAllPrefs(mAvailableDevicesCategory); - removeCachedPrefs(mAvailableDevicesCategory); - } - @VisibleForTesting void showBluetoothTurnedOnToast() { Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 5256f3d6596..039080b26ba 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * 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. @@ -35,6 +35,8 @@ import android.view.View; import android.widget.ImageView; import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; @@ -52,6 +54,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.Set; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; /** * BluetoothDevicePreference is the preference type used to display each remote @@ -79,7 +82,9 @@ public final class BluetoothDevicePreference extends GearPreference { @VisibleForTesting BluetoothAdapter mBluetoothAdapter; private final boolean mShowDevicesWithoutNames; - private final long mCurrentTime; + @NonNull + private static final AtomicInteger sNextId = new AtomicInteger(); + private final int mId; private final int mType; private AlertDialog mDisconnectDialog; @@ -127,8 +132,9 @@ public final class BluetoothDevicePreference extends GearPreference { mCachedDevice = cachedDevice; mCallback = new BluetoothDevicePreferenceCallback(); - mCurrentTime = System.currentTimeMillis(); + mId = sNextId.getAndIncrement(); mType = type; + setVisible(false); onPreferenceAttributesChanged(); } @@ -229,35 +235,41 @@ public final class BluetoothDevicePreference extends GearPreference { @SuppressWarnings("FutureReturnValueIgnored") void onPreferenceAttributesChanged() { - Pair pair = mCachedDevice.getDrawableWithDescription(); - setIcon(pair.first); - contentDescription = pair.second; - - /* - * The preference framework takes care of making sure the value has - * changed before proceeding. It will also call notifyChanged() if - * any preference info has changed from the previous value. - */ - setTitle(mCachedDevice.getName()); try { ThreadUtils.postOnBackgroundThread(() -> { + @Nullable String name = mCachedDevice.getName(); // Null check is done at the framework - ThreadUtils.postOnMainThread(() -> setSummary(getConnectionSummary())); + @Nullable String connectionSummary = getConnectionSummary(); + @NonNull Pair pair = mCachedDevice.getDrawableWithDescription(); + boolean isBusy = mCachedDevice.isBusy(); + // Device is only visible in the UI if it has a valid name besides MAC address or + // when user allows showing devices without user-friendly name in developer settings + boolean isVisible = + mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName(); + + ThreadUtils.postOnMainThread(() -> { + /* + * The preference framework takes care of making sure the value has + * changed before proceeding. It will also call notifyChanged() if + * any preference info has changed from the previous value. + */ + setTitle(name); + setSummary(connectionSummary); + setIcon(pair.first); + contentDescription = pair.second; + // Used to gray out the item + setEnabled(!isBusy); + setVisible(isVisible); + + // This could affect ordering, so notify that + if (mNeedNotifyHierarchyChanged) { + notifyHierarchyChanged(); + } + }); }); } catch (RejectedExecutionException e) { Log.w(TAG, "Handler thread unavailable, skipping getConnectionSummary!"); } - // Used to gray out the item - setEnabled(!mCachedDevice.isBusy()); - - // Device is only visible in the UI if it has a valid name besides MAC address or when user - // allows showing devices without user-friendly name in developer settings - setVisible(mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName()); - - // This could affect ordering, so notify that - if (mNeedNotifyHierarchyChanged) { - notifyHierarchyChanged(); - } } @Override @@ -311,7 +323,7 @@ public final class BluetoothDevicePreference extends GearPreference { return mCachedDevice .compareTo(((BluetoothDevicePreference) another).mCachedDevice); case SortType.TYPE_FIFO: - return mCurrentTime > ((BluetoothDevicePreference) another).mCurrentTime ? 1 : -1; + return mId > ((BluetoothDevicePreference) another).mId ? 1 : -1; default: return super.compareTo(another); } diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java deleted file mode 100644 index a4a98917974..00000000000 --- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (C) 2011 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.bluetooth; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -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.os.Bundle; -import android.os.SystemProperties; -import android.text.BidiFormatter; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceGroup; - -import com.android.settings.R; -import com.android.settings.dashboard.RestrictedDashboardFragment; -import com.android.settingslib.bluetooth.BluetoothCallback; -import com.android.settingslib.bluetooth.BluetoothDeviceFilter; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - -/** - * Parent class for settings fragments that contain a list of Bluetooth - * devices. - * - * @see DevicePickerFragment - */ -// TODO: Refactor this fragment -public abstract class DeviceListPreferenceFragment extends - RestrictedDashboardFragment implements BluetoothCallback { - - private static final String TAG = "DeviceListPreferenceFragment"; - - private static final String KEY_BT_SCAN = "bt_scan"; - - // Copied from BluetoothDeviceNoNamePreferenceController.java - private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = - "persist.bluetooth.showdeviceswithoutnames"; - - private BluetoothDeviceFilter.Filter mFilter; - private List mLeScanFilters; - private ScanCallback mScanCallback; - - @VisibleForTesting - protected boolean mScanEnabled; - - protected BluetoothDevice mSelectedDevice; - - protected BluetoothAdapter mBluetoothAdapter; - protected LocalBluetoothManager mLocalManager; - protected CachedBluetoothDeviceManager mCachedDeviceManager; - - @VisibleForTesting - protected PreferenceGroup mDeviceListGroup; - - protected final HashMap mDevicePreferenceMap = - new HashMap<>(); - protected final List mSelectedList = new ArrayList<>(); - - protected boolean mShowDevicesWithoutNames; - - public DeviceListPreferenceFragment(String restrictedKey) { - super(restrictedKey); - mFilter = BluetoothDeviceFilter.ALL_FILTER; - } - - protected final void setFilter(BluetoothDeviceFilter.Filter filter) { - mFilter = filter; - } - - protected final void setFilter(int filterType) { - mFilter = BluetoothDeviceFilter.getFilter(filterType); - } - - /** - * Sets the bluetooth device scanning filter with {@link ScanFilter}s. It will change to start - * {@link BluetoothLeScanner} which will scan BLE device only. - * - * @param leScanFilters list of settings to filter scan result - */ - protected void setFilter(List leScanFilters) { - mFilter = null; - mLeScanFilters = leScanFilters; - } - - @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 = BluetoothAdapter.getDefaultAdapter(); - mCachedDeviceManager = mLocalManager.getCachedDeviceManager(); - mShowDevicesWithoutNames = SystemProperties.getBoolean( - BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); - - initPreferencesFromPreferenceScreen(); - - mDeviceListGroup = (PreferenceCategory) findPreference(getDeviceListKey()); - } - - /** find and update preference that already existed in preference screen */ - protected abstract void initPreferencesFromPreferenceScreen(); - - @Override - public void onStart() { - super.onStart(); - if (mLocalManager == null || isUiRestricted()) return; - - mLocalManager.setForegroundActivity(getActivity()); - mLocalManager.getEventManager().registerCallback(this); - } - - @Override - public void onStop() { - super.onStop(); - if (mLocalManager == null || isUiRestricted()) { - return; - } - - removeAllDevices(); - mLocalManager.setForegroundActivity(null); - mLocalManager.getEventManager().unregisterCallback(this); - } - - void removeAllDevices() { - mDevicePreferenceMap.clear(); - mDeviceListGroup.removeAll(); - } - - void addCachedDevices() { - Collection cachedDevices = - mCachedDeviceManager.getCachedDevicesCopy(); - for (CachedBluetoothDevice cachedDevice : cachedDevices) { - onDeviceAdded(cachedDevice); - } - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (KEY_BT_SCAN.equals(preference.getKey())) { - startScanning(); - return true; - } - - if (preference instanceof BluetoothDevicePreference) { - BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference; - CachedBluetoothDevice device = btPreference.getCachedDevice(); - mSelectedDevice = device.getDevice(); - mSelectedList.add(mSelectedDevice); - onDevicePreferenceClick(btPreference); - return true; - } - - return super.onPreferenceTreeClick(preference); - } - - protected void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { - btPreference.onClicked(); - } - - @Override - public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { - if (mDevicePreferenceMap.get(cachedDevice) != null) { - return; - } - - // Prevent updates while the list shows one of the state messages - if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { - return; - } - - if (mFilter != null && mFilter.matches(cachedDevice.getDevice())) { - createDevicePreference(cachedDevice); - } - } - - void createDevicePreference(CachedBluetoothDevice cachedDevice) { - if (mDeviceListGroup == null) { - Log.w(TAG, "Trying to create a device preference before the list group/category " - + "exists!"); - 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); - //Set hideSecondTarget is true if it's bonded device. - preference.hideSecondTarget(true); - mDeviceListGroup.addPreference(preference); - } - - initDevicePreference(preference); - mDevicePreferenceMap.put(cachedDevice, preference); - } - - protected void initDevicePreference(BluetoothDevicePreference preference) { - // Does nothing by default - } - - @VisibleForTesting - void updateFooterPreference(Preference myDevicePreference) { - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - - myDevicePreference.setTitle(getString( - R.string.bluetooth_footer_mac_message, - bidiFormatter.unicodeWrap(mBluetoothAdapter.getAddress()))); - } - - @Override - public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice); - if (preference != null) { - mDeviceListGroup.removePreference(preference); - } - } - - @VisibleForTesting - protected void enableScanning() { - // BluetoothAdapter already handles repeated scan requests - if (!mScanEnabled) { - startScanning(); - mScanEnabled = true; - } - } - - @VisibleForTesting - protected void disableScanning() { - if (mScanEnabled) { - stopScanning(); - mScanEnabled = false; - } - } - - @Override - public void onScanningStateChanged(boolean started) { - if (!started && mScanEnabled) { - startScanning(); - } - } - - /** - * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices - */ - public abstract String getDeviceListKey(); - - public boolean shouldShowDevicesWithoutNames() { - return mShowDevicesWithoutNames; - } - - @VisibleForTesting - void startScanning() { - if (mFilter != null) { - startClassicScanning(); - } else if (mLeScanFilters != null) { - startLeScanning(); - } - - } - - @VisibleForTesting - void stopScanning() { - if (mFilter != null) { - stopClassicScanning(); - } else if (mLeScanFilters != null) { - stopLeScanning(); - } - } - - private void startClassicScanning() { - if (!mBluetoothAdapter.isDiscovering()) { - mBluetoothAdapter.startDiscovery(); - } - } - - private void stopClassicScanning() { - if (mBluetoothAdapter.isDiscovering()) { - mBluetoothAdapter.cancelDiscovery(); - } - } - - private void startLeScanning() { - final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); - final ScanSettings settings = new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build(); - mScanCallback = new ScanCallback() { - @Override - public void onScanResult(int callbackType, ScanResult result) { - final BluetoothDevice device = result.getDevice(); - CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device); - if (cachedDevice == null) { - cachedDevice = mCachedDeviceManager.addDevice(device); - } - // Only add device preference when it's not found in the map and there's no other - // state message showing in the list - if (mDevicePreferenceMap.get(cachedDevice) == null - && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { - createDevicePreference(cachedDevice); - } - } - - @Override - public void onScanFailed(int errorCode) { - Log.w(TAG, "BLE Scan failed with error code " + errorCode); - } - }; - scanner.startScan(mLeScanFilters, settings, mScanCallback); - } - - private void stopLeScanning() { - final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); - if (scanner != null) { - scanner.stopScan(mScanCallback); - } - } -} diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt new file mode 100644 index 00000000000..9c86e4398f6 --- /dev/null +++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt @@ -0,0 +1,348 @@ +/* + * 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.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +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.os.Bundle +import android.os.SystemProperties +import android.text.BidiFormatter +import android.util.Log +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceGroup +import com.android.settings.R +import com.android.settings.dashboard.RestrictedDashboardFragment +import com.android.settingslib.bluetooth.BluetoothCallback +import com.android.settingslib.bluetooth.BluetoothDeviceFilter +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager +import com.android.settingslib.bluetooth.LocalBluetoothManager +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Parent class for settings fragments that contain a list of Bluetooth devices. + * + * @see DevicePickerFragment + * + * TODO: Refactor this fragment + */ +abstract class DeviceListPreferenceFragment(restrictedKey: String?) : + RestrictedDashboardFragment(restrictedKey), BluetoothCallback { + + private var filter: BluetoothDeviceFilter.Filter? = BluetoothDeviceFilter.ALL_FILTER + private var leScanFilters: List? = null + + @JvmField + @VisibleForTesting + var mScanEnabled = false + + @JvmField + var mSelectedDevice: BluetoothDevice? = null + + @JvmField + var mBluetoothAdapter: BluetoothAdapter? = null + + @JvmField + var mLocalManager: LocalBluetoothManager? = null + + @JvmField + var mCachedDeviceManager: CachedBluetoothDeviceManager? = null + + @JvmField + @VisibleForTesting + var mDeviceListGroup: PreferenceGroup? = null + + @VisibleForTesting + val devicePreferenceMap = + ConcurrentHashMap() + + @JvmField + val mSelectedList: MutableList = ArrayList() + + private var showDevicesWithoutNames = false + + protected fun setFilter(filter: BluetoothDeviceFilter.Filter?) { + this.filter = filter + } + + protected fun setFilter(filterType: Int) { + filter = BluetoothDeviceFilter.getFilter(filterType) + } + + /** + * Sets the bluetooth device scanning filter with [ScanFilter]s. It will change to start + * [BluetoothLeScanner] which will scan BLE device only. + * + * @param leScanFilters list of settings to filter scan result + */ + fun setFilter(leScanFilters: List?) { + filter = null + this.leScanFilters = leScanFilters + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mLocalManager = Utils.getLocalBtManager(activity) + if (mLocalManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device") + return + } + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + mCachedDeviceManager = mLocalManager!!.cachedDeviceManager + showDevicesWithoutNames = SystemProperties.getBoolean( + BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false + ) + initPreferencesFromPreferenceScreen() + mDeviceListGroup = findPreference(deviceListKey) as PreferenceCategory + } + + /** find and update preference that already existed in preference screen */ + protected abstract fun initPreferencesFromPreferenceScreen() + + private var lifecycleScope: LifecycleCoroutineScope? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + lifecycleScope = viewLifecycleOwner.lifecycleScope + } + + override fun onStart() { + super.onStart() + if (mLocalManager == null || isUiRestricted) return + mLocalManager!!.foregroundActivity = activity + mLocalManager!!.eventManager.registerCallback(this) + } + + override fun onStop() { + super.onStop() + if (mLocalManager == null || isUiRestricted) { + return + } + removeAllDevices() + mLocalManager!!.foregroundActivity = null + mLocalManager!!.eventManager.unregisterCallback(this) + } + + fun removeAllDevices() { + devicePreferenceMap.clear() + mDeviceListGroup!!.removeAll() + } + + fun addCachedDevices() { + lifecycleScope?.launch { + withContext(Dispatchers.Default) { + val cachedDevices = mCachedDeviceManager!!.cachedDevicesCopy + for (cachedDevice in cachedDevices) { + onDeviceAdded(cachedDevice) + } + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + if (KEY_BT_SCAN == preference.key) { + startScanning() + return true + } + if (preference is BluetoothDevicePreference) { + val device = preference.cachedDevice.device + mSelectedDevice = device + mSelectedList.add(device) + onDevicePreferenceClick(preference) + return true + } + return super.onPreferenceTreeClick(preference) + } + + protected open fun onDevicePreferenceClick(btPreference: BluetoothDevicePreference) { + btPreference.onClicked() + } + + override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) { + lifecycleScope?.launch { + addDevice(cachedDevice) + } + } + + private suspend fun addDevice(cachedDevice: CachedBluetoothDevice) = + withContext(Dispatchers.Default) { + // Prevent updates while the list shows one of the state messages + if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON && + filter?.matches(cachedDevice.device) == true + ) { + createDevicePreference(cachedDevice) + } + } + + private suspend fun createDevicePreference(cachedDevice: CachedBluetoothDevice) { + if (mDeviceListGroup == null) { + Log.w( + TAG, + "Trying to create a device preference before the list group/category exists!", + ) + return + } + // Only add device preference when it's not found in the map and there's no other state + // message showing in the list + val preference = devicePreferenceMap.computeIfAbsent(cachedDevice) { + BluetoothDevicePreference( + prefContext, + cachedDevice, + showDevicesWithoutNames, + BluetoothDevicePreference.SortType.TYPE_FIFO, + ).apply { + key = cachedDevice.device.address + //Set hideSecondTarget is true if it's bonded device. + hideSecondTarget(true) + } + } + withContext(Dispatchers.Main) { + mDeviceListGroup!!.addPreference(preference) + initDevicePreference(preference) + } + } + + protected open fun initDevicePreference(preference: BluetoothDevicePreference?) { + // Does nothing by default + } + + @VisibleForTesting + fun updateFooterPreference(myDevicePreference: Preference) { + val bidiFormatter = BidiFormatter.getInstance() + myDevicePreference.title = getString( + R.string.bluetooth_footer_mac_message, + bidiFormatter.unicodeWrap(mBluetoothAdapter!!.address) + ) + } + + override fun onDeviceDeleted(cachedDevice: CachedBluetoothDevice) { + devicePreferenceMap.remove(cachedDevice)?.let { + mDeviceListGroup!!.removePreference(it) + } + } + + @VisibleForTesting + open fun enableScanning() { + // BluetoothAdapter already handles repeated scan requests + if (!mScanEnabled) { + startScanning() + mScanEnabled = true + } + } + + @VisibleForTesting + fun disableScanning() { + if (mScanEnabled) { + stopScanning() + mScanEnabled = false + } + } + + override fun onScanningStateChanged(started: Boolean) { + if (!started && mScanEnabled) { + startScanning() + } + } + + /** + * Return the key of the [PreferenceGroup] that contains the bluetooth devices + */ + abstract val deviceListKey: String + + @VisibleForTesting + open fun startScanning() { + if (filter != null) { + startClassicScanning() + } else if (leScanFilters != null) { + startLeScanning() + } + } + + @VisibleForTesting + open fun stopScanning() { + if (filter != null) { + stopClassicScanning() + } else if (leScanFilters != null) { + stopLeScanning() + } + } + + private fun startClassicScanning() { + if (!mBluetoothAdapter!!.isDiscovering) { + mBluetoothAdapter!!.startDiscovery() + } + } + + private fun stopClassicScanning() { + if (mBluetoothAdapter!!.isDiscovering) { + mBluetoothAdapter!!.cancelDiscovery() + } + } + + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + lifecycleScope?.launch { + withContext(Dispatchers.Default) { + if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON) { + val device = result.device + val cachedDevice = mCachedDeviceManager!!.findDevice(device) + ?: mCachedDeviceManager!!.addDevice(device) + createDevicePreference(cachedDevice) + } + } + } + } + + override fun onScanFailed(errorCode: Int) { + Log.w(TAG, "BLE Scan failed with error code $errorCode") + } + } + + private fun startLeScanning() { + val scanner = mBluetoothAdapter!!.bluetoothLeScanner + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + scanner.startScan(leScanFilters, settings, scanCallback) + } + + private fun stopLeScanning() { + val scanner = mBluetoothAdapter!!.bluetoothLeScanner + scanner?.stopScan(scanCallback) + } + + companion object { + private const val TAG = "DeviceListPreferenceFragment" + private const val KEY_BT_SCAN = "bt_scan" + + // Copied from BluetoothDeviceNoNamePreferenceController.java + private const val BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = + "persist.bluetooth.showdeviceswithoutnames" + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index 184f5212e77..7c598e00e42 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -202,7 +202,7 @@ public class BluetoothDevicePairingDetailBaseTest { new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS); - mFragment.mDevicePreferenceMap.put(mCachedBluetoothDevice, preference); + mFragment.getDevicePreferenceMap().put(mCachedBluetoothDevice, preference); when(mCachedBluetoothDevice.isConnected()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(device); @@ -210,7 +210,7 @@ public class BluetoothDevicePairingDetailBaseTest { mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); - assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0); + assertThat(mFragment.getDevicePreferenceMap().size()).isEqualTo(0); } @Test @@ -221,7 +221,7 @@ public class BluetoothDevicePairingDetailBaseTest { true, BluetoothDevicePreference.SortType.TYPE_FIFO); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS); final BluetoothDevice device2 = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); - mFragment.mDevicePreferenceMap.put(mCachedBluetoothDevice, preference); + mFragment.getDevicePreferenceMap().put(mCachedBluetoothDevice, preference); when(mCachedBluetoothDevice.isConnected()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(device); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java index 5fbfee8b50d..ce67051a7c7 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDetailTest.java @@ -27,7 +27,12 @@ import static org.mockito.Mockito.verify; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; @@ -53,6 +58,20 @@ public class BluetoothPairingDetailTest { private final Context mContext = ApplicationProvider.getApplicationContext(); + private final Lifecycle mFakeLifecycle = new Lifecycle() { + @Override + public void addObserver(@NonNull LifecycleObserver observer) {} + + @Override + public void removeObserver(@NonNull LifecycleObserver observer) {} + + @NonNull + @Override + public State getCurrentState() { + return State.CREATED; + } + }; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private LocalBluetoothManager mLocalManager; @Mock(answer = Answers.RETURNS_DEEP_STUBS) @@ -74,6 +93,8 @@ public class BluetoothPairingDetailTest { .findPreference(BluetoothPairingDetail.KEY_AVAIL_DEVICES); doReturn(mFooterPreference).when(mFragment) .findPreference(BluetoothPairingDetail.KEY_FOOTER_PREF); + doReturn(new View(mContext)).when(mFragment).getView(); + doReturn((LifecycleOwner) () -> mFakeLifecycle).when(mFragment).getViewLifecycleOwner(); doReturn(Collections.emptyList()).when(mDeviceManager).getCachedDevicesCopy(); mFragment.mBluetoothAdapter = mBluetoothAdapter; @@ -82,7 +103,7 @@ public class BluetoothPairingDetailTest { mFragment.mDeviceListGroup = mAvailableDevicesCategory; mFragment.onViewCreated(mFragment.getView(), Bundle.EMPTY); } -// + @Test public void initPreferencesFromPreferenceScreen_findPreferences() { mFragment.initPreferencesFromPreferenceScreen();