From 0ec34ce1d79b302559877f379306fbfc475cf400 Mon Sep 17 00:00:00 2001 From: petsjonkin Date: Thu, 6 Feb 2025 15:43:20 +0000 Subject: [PATCH 01/12] Updating NightLight Intensity slider to SliderPreference component Bug: b/349670337 Test: manual verification Flag: com.android.settingslib.widget.theme.flags.is_expressive_design_enabled Change-Id: If0946271e2ea274f1ecb3a57a1978f733dbcada8 --- res/xml/night_display_settings.xml | 2 +- .../NightDisplayIntensityPreferenceController.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/res/xml/night_display_settings.xml b/res/xml/night_display_settings.xml index 95d503415f5..d75619d39e1 100644 --- a/res/xml/night_display_settings.xml +++ b/res/xml/night_display_settings.xml @@ -52,7 +52,7 @@ android:title="@string/night_display_end_time_title" settings:controller="com.android.settings.display.NightDisplayCustomEndTimePreferenceController"/> - Date: Fri, 21 Feb 2025 02:26:29 +0000 Subject: [PATCH 02/12] Map pointer speed slider on mouse page to control mouse speed Currently, the pointer slider on the mouse settings page is actually controlling touchpad pointer speed. Modify this to instead control the mouse pointer speed. Bug: 398034722 Test: Manually on device Flag: com.android.settings.keyboard.keyboard_and_touchpad_a11y_new_page_enabled Change-Id: I66cec4ef0a758e250bab89c194073eb408402431 --- res/xml/mouse_settings.xml | 6 +- ...MousePointerSpeedPreferenceController.java | 80 +++++++++++++ ...ePointerSpeedPreferenceControllerTest.java | 106 ++++++++++++++++++ 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/com/android/settings/inputmethod/MousePointerSpeedPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/inputmethod/MousePointerSpeedPreferenceControllerTest.java diff --git a/res/xml/mouse_settings.xml b/res/xml/mouse_settings.xml index e4b3f1c8f79..ec1c39d31d2 100644 --- a/res/xml/mouse_settings.xml +++ b/res/xml/mouse_settings.xml @@ -28,11 +28,11 @@ settings:controller="com.android.settings.inputmethod.MousePointerAccelerationPreferenceController" /> + settings:controller="com.android.settings.inputmethod.MousePointerSpeedPreferenceController"/> getMax()) { + return false; + } + InputSettings.setPointerSpeed(mContext, position); + mMetricsFeatureProvider.action( + mContext, SettingsEnums.ACTION_GESTURE_POINTER_SPEED_CHANGED, position); + return true; + } + + @Override + public int getSliderPosition() { + return InputSettings.getPointerSpeed(mContext); + } + + @Override + public int getMin() { + return InputSettings.MIN_POINTER_SPEED; + } + + @Override + public int getMax() { + return InputSettings.MAX_POINTER_SPEED; + } +} diff --git a/tests/robotests/src/com/android/settings/inputmethod/MousePointerSpeedPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/MousePointerSpeedPreferenceControllerTest.java new file mode 100644 index 00000000000..82afec2693a --- /dev/null +++ b/tests/robotests/src/com/android/settings/inputmethod/MousePointerSpeedPreferenceControllerTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2025 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.inputmethod; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.hardware.input.InputSettings; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.FakeFeatureFactory; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link MousePointerSpeedPreferenceController} */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + com.android.settings.testutils.shadow.ShadowSystemSettings.class, +}) +public class MousePointerSpeedPreferenceControllerTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + private static final String PREFERENCE_KEY = "pointer_speed"; + private static final String SETTING_KEY = Settings.System.POINTER_SPEED; + + private MousePointerSpeedPreferenceController mController; + private int mDefaultSpeed; + private FakeFeatureFactory mFeatureFactory; + + @Before + public void setUp() { + Context context = ApplicationProvider.getApplicationContext(); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mController = new MousePointerSpeedPreferenceController(context, PREFERENCE_KEY); + mDefaultSpeed = Settings.System.getIntForUser( + context.getContentResolver(), + SETTING_KEY, + InputSettings.DEFAULT_POINTER_SPEED, + UserHandle.USER_CURRENT); + } + + @Test + public void setSliderPosition_speedValue1_shouldReturnTrue() { + int inputSpeed = 1; + + boolean result = mController.setSliderPosition(inputSpeed); + + assertThat(result).isTrue(); + assertThat(mController.getSliderPosition()).isEqualTo(inputSpeed); + verify(mFeatureFactory.metricsFeatureProvider).action( + any(), + eq(SettingsEnums.ACTION_GESTURE_POINTER_SPEED_CHANGED), + eq(1)); + } + + @Test + public void setSliderPosition_speedValueOverMaxValue_shouldReturnFalse() { + int inputSpeed = InputSettings.MAX_POINTER_SPEED + 1; + + boolean result = mController.setSliderPosition(inputSpeed); + + assertThat(result).isFalse(); + assertThat(mController.getSliderPosition()).isEqualTo(mDefaultSpeed); + } + + @Test + public void setSliderPosition_speedValueOverMinValue_shouldReturnFalse() { + int inputSpeed = InputSettings.MIN_POINTER_SPEED - 1; + + boolean result = mController.setSliderPosition(inputSpeed); + + assertThat(result).isFalse(); + assertThat(mController.getSliderPosition()).isEqualTo(mDefaultSpeed); + } +} From 52874a641adf21faf7cfce11d02798fed567b3fe Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Fri, 7 Feb 2025 06:43:45 +0000 Subject: [PATCH 03/12] Support the ability to enroll face unlock first Add a new EXTRA value to indicate whehter the face enrollment should be launched first. Bug: 370940762 Test: atest BiometricEnrollActivityTest Flag: com.android.settings.flags.biometrics_onboarding_education Change-Id: I7c85212c7fbcc6fe9dd53a26515412623c80ecbf --- .../biometrics/BiometricEnrollActivity.java | 31 +++++++++++++--- .../settings/biometrics/BiometricUtils.java | 5 +-- .../BiometricEnrollActivityTest.java | 37 +++++++++++++++++-- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/biometrics/BiometricEnrollActivity.java b/src/com/android/settings/biometrics/BiometricEnrollActivity.java index ef1970995df..83f23bdcf7a 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollActivity.java +++ b/src/com/android/settings/biometrics/BiometricEnrollActivity.java @@ -107,7 +107,10 @@ public class BiometricEnrollActivity extends InstrumentedActivity { // intent will include this extra containing a bundle of the form: // "modality" -> consented (boolean). public static final String EXTRA_PARENTAL_CONSENT_STATUS = "consent_status"; - + // Whether the face enrollment should be launched first when there are multiple biometrics + // supported. + public static final String EXTRA_LAUNCH_FACE_ENROLL_FIRST = + "launch_face_enroll_first"; private static final String SAVED_STATE_CONFIRMING_CREDENTIALS = "confirming_credentials"; private static final String SAVED_STATE_IS_SINGLE_ENROLLING = "is_single_enrolling"; @@ -130,6 +133,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity { private boolean mIsFingerprintEnrollable = false; private boolean mParentalOptionsRequired = false; private boolean mSkipReturnToParent = false; + private boolean mLaunchFaceEnrollFirst = false; private Bundle mParentalOptions; @Nullable private Long mGkPwHandle; @Nullable private ParentalConsentHelper mParentalConsentHelper; @@ -214,6 +218,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity { mParentalOptionsRequired = intent.getBooleanExtra(EXTRA_REQUIRE_PARENTAL_CONSENT, false); mSkipReturnToParent = intent.getBooleanExtra(EXTRA_SKIP_RETURN_TO_PARENT, false); + mLaunchFaceEnrollFirst = intent.getBooleanExtra(EXTRA_LAUNCH_FACE_ENROLL_FIRST, false); // determine what can be enrolled final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); @@ -221,6 +226,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity { Log.d(TAG, "parentalOptionsRequired: " + mParentalOptionsRequired + ", skipReturnToParent: " + mSkipReturnToParent + + ", launchFaceEnrollFirst: " + mLaunchFaceEnrollFirst + ", isSetupWizard: " + isSetupWizard + ", isMultiSensor: " + isMultiSensor); @@ -356,7 +362,8 @@ public class BiometricEnrollActivity extends InstrumentedActivity { } else if (canUseFace || canUseFingerprint) { if (mGkPwHandle == null) { setOrConfirmCredentialsNow(); - } else if (canUseFingerprint && mIsFingerprintEnrollable) { + } else if (canUseFingerprint && mIsFingerprintEnrollable + && !(canUseFace && mIsFaceEnrollable && mLaunchFaceEnrollFirst)) { launchFingerprintOnlyEnroll(); } else if (canUseFace && mIsFaceEnrollable) { launchFaceOnlyEnroll(); @@ -510,7 +517,8 @@ public class BiometricEnrollActivity extends InstrumentedActivity { int requestCode, int resultCode, Intent data) { Log.d(TAG, "handleOnActivityResultWhileEnrolling, request = " + requestCode + "" - + ", resultCode = " + resultCode); + + ", resultCode = " + resultCode + ", launchFaceEnrollFirst=" + + mLaunchFaceEnrollFirst); switch (requestCode) { case REQUEST_HANDOFF_PARENT: setResult(RESULT_OK, newResultIntent()); @@ -526,7 +534,8 @@ public class BiometricEnrollActivity extends InstrumentedActivity { // SetupFingerprintEnrollIntroduction/FingerprintEnrollmentActivity TransitionHelper.applyForwardTransition(this, TRANSITION_FADE_THROUGH); updateGatekeeperPasswordHandle(data); - if (mIsFingerprintEnrollable) { + if (mIsFingerprintEnrollable + && !(mIsFaceEnrollable && mLaunchFaceEnrollFirst)) { launchFingerprintOnlyEnroll(); } else { launchFaceOnlyEnroll(); @@ -548,7 +557,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity { } if ((resultCode == BiometricEnrollBase.RESULT_SKIP || resultCode == BiometricEnrollBase.RESULT_FINISHED) - && mIsFaceEnrollable) { + && mIsFaceEnrollable && !mLaunchFaceEnrollFirst) { // Apply forward animation during the transition from // SetupFingerprintEnroll*/FingerprintEnrollmentActivity to // SetupFaceEnrollIntroduction @@ -556,6 +565,9 @@ public class BiometricEnrollActivity extends InstrumentedActivity { mIsPreviousEnrollmentCanceled = resultCode != BiometricEnrollBase.RESULT_FINISHED; launchFaceOnlyEnroll(); + } else if (resultCode == Activity.RESULT_CANCELED && mIsFaceEnrollable + && mLaunchFaceEnrollFirst) { + launchFaceOnlyEnroll(); } else { notifySafetyIssueActionLaunchedIfNeeded(resultCode); finishOrLaunchHandToParent(resultCode); @@ -563,7 +575,14 @@ public class BiometricEnrollActivity extends InstrumentedActivity { break; case REQUEST_SINGLE_ENROLL_FACE: mIsSingleEnrolling = false; - if (resultCode == Activity.RESULT_CANCELED && mIsFingerprintEnrollable) { + if ((resultCode == BiometricEnrollBase.RESULT_SKIP + || resultCode == BiometricEnrollBase.RESULT_FINISHED) + && mIsFingerprintEnrollable && mLaunchFaceEnrollFirst) { + mIsPreviousEnrollmentCanceled = + resultCode != BiometricEnrollBase.RESULT_FINISHED; + launchFingerprintOnlyEnroll(); + } else if (resultCode == Activity.RESULT_CANCELED && mIsFingerprintEnrollable + && !mLaunchFaceEnrollFirst) { mIsPreviousEnrollmentCanceled = true; launchFingerprintOnlyEnroll(); } else { diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java index db6abc3c979..21b0fa03f06 100644 --- a/src/com/android/settings/biometrics/BiometricUtils.java +++ b/src/com/android/settings/biometrics/BiometricUtils.java @@ -43,6 +43,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.VerifyCredentialResponse; import com.android.settings.R; import com.android.settings.SetupWizardUtils; +import com.android.settings.biometrics.face.FaceEnroll; import com.android.settings.biometrics.fingerprint.FingerprintEnroll; import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor; @@ -282,9 +283,7 @@ public class BiometricUtils { */ public static Intent getFaceIntroIntent(@NonNull Context context, @NonNull Intent activityIntent) { - final Intent intent = new Intent(context, - FeatureFactory.getFeatureFactory().getFaceFeatureProvider() - .getEnrollActivityClassProvider().getNext()); + final Intent intent = new Intent(context, FaceEnroll.class); WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); return intent; } diff --git a/tests/componenttests/src/com/android/settings/biometrics/BiometricEnrollActivityTest.java b/tests/componenttests/src/com/android/settings/biometrics/BiometricEnrollActivityTest.java index eb28dfbe624..267b5bc4c21 100644 --- a/tests/componenttests/src/com/android/settings/biometrics/BiometricEnrollActivityTest.java +++ b/tests/componenttests/src/com/android/settings/biometrics/BiometricEnrollActivityTest.java @@ -23,6 +23,7 @@ import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; +import static com.android.settings.biometrics.BiometricEnrollActivity.EXTRA_LAUNCH_FACE_ENROLL_FIRST; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT; @@ -39,6 +40,7 @@ import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; @@ -145,7 +147,7 @@ public class BiometricEnrollActivityTest { assumeTrue(mHasFace || mHasFingerprint); setPin(); - final Intent intent = getIntent(true /* useInternal */); + final Intent intent = getIntent(true /* useInternal */, null); LockPatternChecker.verifyCredential(new LockPatternUtils(mContext), LockscreenCredential.createPin(TEST_PIN), UserHandle.myUserId(), LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, (response, timeoutMs) -> { @@ -162,6 +164,26 @@ public class BiometricEnrollActivityTest { } } + @Test + public void launchWithPinAndPwHandle_confirmsPin_firstEnrollmentIsFace() throws Exception { + assumeTrue(mHasFace && mHasFingerprint); + + setPin(); + final Intent intent = getFaceEnrollFirstIntent(); + LockPatternChecker.verifyCredential(new LockPatternUtils(mContext), + LockscreenCredential.createPin(TEST_PIN), UserHandle.myUserId(), + LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, (response, timeoutMs) -> { + assertThat(response.containsGatekeeperPasswordHandle()).isTrue(); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, + response.getGatekeeperPasswordHandle()); + }).get(); + + try (ActivityScenario scenario = + ActivityScenario.launch(intent)) { + intended(hasComponent(FaceEnroll.class.getName())); + } + } + @Test public void launchWithStrongBiometricAllowed_doNotEnrollWeak() throws Exception { assumeTrue(mHasFace || mHasFingerprint); @@ -184,13 +206,22 @@ public class BiometricEnrollActivityTest { } private Intent getIntent() { - return getIntent(false /* useInternal */); + return getIntent(false /* useInternal */, null); } - private Intent getIntent(boolean useInternal) { + private Intent getFaceEnrollFirstIntent() { + final Bundle bundle = new Bundle(); + bundle.putBoolean(EXTRA_LAUNCH_FACE_ENROLL_FIRST, true); + return getIntent(true /* useInternal */, bundle); + } + + private Intent getIntent(boolean useInternal, Bundle bundle) { final Intent intent = new Intent(mContext, useInternal ? BiometricEnrollActivity.InternalActivity.class : BiometricEnrollActivity.class); intent.setAction(ACTION_BIOMETRIC_ENROLL); + if (bundle != null && !bundle.isEmpty()) { + intent.putExtras(bundle); + } return intent; } From 25c54af9826185ca68e268170c2192b64aa5b76b Mon Sep 17 00:00:00 2001 From: Allen Su Date: Mon, 3 Mar 2025 17:39:55 +0000 Subject: [PATCH 04/12] Add Confirmation Dialog Bug: 388418881 Test: atest LocaleListEditorTest Flag: EXEMPT bug fix Change-Id: Iacec13b9ef6edd44f9548354888cac39fd6b2892 --- .../localepicker/LocaleDialogFragment.java | 9 +- .../LocaleDragAndDropAdapter.java | 15 +++- .../LocaleLinearLayoutManager.java | 2 +- .../localepicker/LocaleListEditor.java | 82 +++++++++++++++---- .../localepicker/LocaleListEditorTest.java | 69 ++++++++++++++-- 5 files changed, 145 insertions(+), 32 deletions(-) diff --git a/src/com/android/settings/localepicker/LocaleDialogFragment.java b/src/com/android/settings/localepicker/LocaleDialogFragment.java index a3a4b8fee72..5c7958a29ca 100644 --- a/src/com/android/settings/localepicker/LocaleDialogFragment.java +++ b/src/com/android/settings/localepicker/LocaleDialogFragment.java @@ -26,11 +26,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; @@ -57,6 +52,7 @@ public class LocaleDialogFragment extends InstrumentedDialogFragment { static final String ARG_DIALOG_TYPE = "arg_dialog_type"; static final String ARG_TARGET_LOCALE = "arg_target_locale"; static final String ARG_SHOW_DIALOG = "arg_show_dialog"; + static final String ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED = "arg_show_dialog_for_not_translated"; private boolean mShouldKeepDialog; private OnBackInvokedDispatcher mBackDispatcher; @@ -185,6 +181,7 @@ public class LocaleDialogFragment extends InstrumentedDialogFragment { private final int mDialogType; private final LocaleStore.LocaleInfo mLocaleInfo; private final MetricsFeatureProvider mMetricsFeatureProvider; + private final boolean mShowDialogForNotTranslated; private LocaleListEditor mParent; @@ -194,6 +191,7 @@ public class LocaleDialogFragment extends InstrumentedDialogFragment { mContext = context; Bundle arguments = dialogFragment.getArguments(); mDialogType = arguments.getInt(ARG_DIALOG_TYPE); + mShowDialogForNotTranslated = arguments.getBoolean(ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED); mLocaleInfo = (LocaleStore.LocaleInfo) arguments.getSerializable(ARG_TARGET_LOCALE); mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); @@ -215,6 +213,7 @@ public class LocaleDialogFragment extends InstrumentedDialogFragment { bundle.putInt(ARG_DIALOG_TYPE, mDialogType); bundle.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE, mLocaleInfo); intent.putExtras(bundle); + intent.putExtra(ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED, mShowDialogForNotTranslated); mParent.onActivityResult(mDialogType, result, intent); mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CHANGE_LANGUAGE, changed); diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index 907fe7bd722..af8b6681cae 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -364,12 +364,25 @@ class LocaleDragAndDropAdapter } public void notifyListChanged(LocaleStore.LocaleInfo localeInfo) { - if (!localeInfo.getLocale().equals(mCacheItemList.get(0).getLocale())) { + if (listChanged()) { mFeedItemList = new ArrayList<>(mCacheItemList); notifyDataSetChanged(); } } + private boolean listChanged() { + if (mFeedItemList.size() == mCacheItemList.size()) { + for (int i = 0; i < mFeedItemList.size(); i++) { + if (!mFeedItemList.get(i).getLocale().equals(mCacheItemList.get(i).getLocale())) { + return true; + } + } + return false; + } else { + return true; + } + } + public void setCacheItemList() { mCacheItemList = new ArrayList<>(mFeedItemList); } diff --git a/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java b/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java index a7ebe32b841..df0af6392fd 100644 --- a/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java +++ b/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java @@ -151,7 +151,7 @@ public class LocaleLinearLayoutManager extends LinearLayoutManager { } if (result) { - mLocaleListEditor.showConfirmDialog(false, mAdapter.getFeedItemList().get(0)); + mLocaleListEditor.showConfirmDialog(mAdapter.getFeedItemList().get(0), null); } return result; } diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index b1f005a79ab..e2da851f8e5 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -44,6 +44,7 @@ import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; @@ -235,7 +236,9 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View localeInfo = mAdapter.getFeedItemList().get(0); if (resultCode == Activity.RESULT_OK) { mAdapter.doTheUpdate(); - if (!localeInfo.isTranslated()) { + boolean showNotTranslatedDialog = data.getBooleanExtra( + LocaleDialogFragment.ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED, true); + if (showNotTranslatedDialog && !localeInfo.isTranslated()) { Bundle args = new Bundle(); args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE); @@ -428,13 +431,10 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View // to remove. mRemoveMode = false; mShowingRemoveDialog = false; - LocaleStore.LocaleInfo firstLocale = - mAdapter.getFeedItemList().get(0); + Locale defaultBeforeRemoval = Locale.getDefault(); mAdapter.removeChecked(); - boolean isFirstRemoved = - firstLocale != mAdapter.getFeedItemList().get(0); - showConfirmDialog(isFirstRemoved, isFirstRemoved ? firstLocale - : mAdapter.getFeedItemList().get(0)); + showConfirmDialog(mAdapter.getFeedItemList().get(0), + defaultBeforeRemoval); setRemoveMode(false); dialogHelper.getDialog().dismiss(); }) @@ -520,27 +520,73 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { - showConfirmDialog(false, mAdapter.getFeedItemList().get(0)); + showConfirmDialog(mAdapter.getFeedItemList().get(0), null); } return false; } - public void showConfirmDialog(boolean isFirstRemoved, LocaleStore.LocaleInfo localeInfo) { + protected void showConfirmDialog(LocaleStore.LocaleInfo localeInfo, + @Nullable Locale defaultLocaleBeforeRemoval) { Locale currentSystemLocale = LocalePicker.getLocales().get(0); if (!localeInfo.getLocale().equals(currentSystemLocale)) { - final LocaleDialogFragment localeDialogFragment = - LocaleDialogFragment.newInstance(); - Bundle args = new Bundle(); - args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); - args.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE, - isFirstRemoved ? LocaleStore.getLocaleInfo(currentSystemLocale) : localeInfo); - localeDialogFragment.setArguments(args); - localeDialogFragment.show(mFragmentManager, TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT); + displayDialogFragment(localeInfo, true); } else { - mAdapter.doTheUpdate(); + if (!localeInfo.isTranslated()) { + if (defaultLocaleBeforeRemoval == null) { + showDialogDueToDragAndDrop(); + } else { + showDialogDueToRemoval(defaultLocaleBeforeRemoval); + } + } else { + mAdapter.doTheUpdate(); + } } } + private void showDialogDueToDragAndDrop() { + LocaleStore.LocaleInfo newLocale = mAdapter.getFeedItemList().stream().filter( + i -> i.isTranslated()).findFirst().orElse(null); + if (newLocale == null) { + return; + } + LocaleStore.LocaleInfo oldLocale = null; + final LocaleList localeList = LocalePicker.getLocales(); + for (int i = 0; i < localeList.size(); i++) { + LocaleStore.LocaleInfo temp = LocaleStore.getLocaleInfo(localeList.get(i)); + if (temp.isTranslated()) { + oldLocale = temp; + break; + } + } + if (oldLocale != null && !newLocale.getLocale().equals( + oldLocale.getLocale())) { + displayDialogFragment(newLocale, false); + } + } + + private void showDialogDueToRemoval(Locale preDefault) { + if (preDefault == null) { + return; + } + LocaleStore.LocaleInfo currentDefault = mAdapter.getFeedItemList().stream().filter( + i -> i.isTranslated()).findFirst().orElse(null); + if (currentDefault != null && !preDefault.equals(currentDefault.getLocale())) { + displayDialogFragment(currentDefault, false); + } + } + + private void displayDialogFragment(LocaleStore.LocaleInfo localeInfo, + boolean showDialogForNotTranslated) { + final LocaleDialogFragment localeDialogFragment = LocaleDialogFragment.newInstance(); + Bundle args = new Bundle(); + args.putBoolean(LocaleDialogFragment.ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED, + showDialogForNotTranslated); + args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); + args.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE, localeInfo); + localeDialogFragment.setArguments(args); + localeDialogFragment.show(mFragmentManager, TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT); + } + // Hide the "Remove" menu if there is only one locale in the list, show it otherwise // This is called when the menu is first created, and then one add / remove locale private void updateVisibilityOfRemoveMenu() { diff --git a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java index 4272afe8eba..22d39e37c12 100644 --- a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java +++ b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java @@ -36,7 +36,6 @@ import android.app.Activity; import android.app.Dialog; import android.app.IActivityManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; @@ -100,6 +99,8 @@ public class LocaleListEditorTest { public final MockitoRule mMockitoRule = MockitoJUnit.rule(); private static final String ARG_DIALOG_TYPE = "arg_dialog_type"; + private static final String + ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED = "arg_show_dialog_for_not_translated"; private static final String TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT = "dialog_confirm_system_default"; private static final String TAG_DIALOG_NOT_AVAILABLE = "dialog_not_available_locale"; private static final String TAG_DIALOG_ADD_SYSTEM_LOCALE = "dialog_add_system_locale"; @@ -123,6 +124,10 @@ public class LocaleListEditorTest { @Mock private LocaleStore.LocaleInfo mLocaleInfo; @Mock + private LocaleStore.LocaleInfo mLocaleInfo1; + @Mock + private LocaleStore.LocaleInfo mLocaleInfo2; + @Mock private FragmentManager mFragmentManager; @Mock private FragmentTransaction mFragmentTransaction; @@ -270,7 +275,7 @@ public class LocaleListEditorTest { public void showConfirmDialog_systemLocaleSelected_shouldShowLocaleChangeDialog() throws Exception { //pre-condition - setUpLocaleConditions(); + setUpLocaleConditions(true); final Configuration config = new Configuration(); config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US"))); when(mActivityService.getConfiguration()).thenReturn(config); @@ -299,6 +304,41 @@ public class LocaleListEditorTest { eq(TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT)); } + @Test + public void showConfirmDialog_2ndLocaleSelected_shouldShowLocaleChangeDialog() + throws Exception { + //pre-condition + Locale.setDefault(Locale.forLanguageTag("en-US")); + setUpLocaleConditions2(); + final Configuration config = new Configuration(); + config.setLocales((LocaleList.forLanguageTags("blo-BJ,en-US,zh-TW"))); + when(mActivityService.getConfiguration()).thenReturn(config); + when(mAdapter.getFeedItemList()).thenReturn(mLocaleList); + when(mAdapter.getCheckedCount()).thenReturn(1); + when(mAdapter.getItemCount()).thenReturn(3); + when(mAdapter.isFirstLocaleChecked()).thenReturn(false); + ReflectionHelpers.setField(mLocaleListEditor, "mRemoveMode", true); + ReflectionHelpers.setField(mLocaleListEditor, "mShowingRemoveDialog", true); + + //launch the first dialog + mLocaleListEditor.showRemoveLocaleWarningDialog(); + + final Dialog dialog = ShadowDialog.getLatestDialog(); + + assertThat(dialog).isNotNull(); + + // click the remove button + dialog.findViewById(R.id.button_ok).performClick(); + ShadowLooper.idleMainLooper(); + + assertThat(dialog.isShowing()).isFalse(); + + // check the second dialog is showing + verify(mFragmentTransaction).add(any(LocaleDialogFragment.class), + eq(TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT)); + } + + @Test public void mayAppendUnicodeTags_appendUnicodeTags_success() { LocaleStore.LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag("en-US")); @@ -315,7 +355,8 @@ public class LocaleListEditorTest { Bundle bundle = new Bundle(); bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); mIntent.putExtras(bundle); - setUpLocaleConditions(); + mIntent.putExtra(ARG_SHOW_DIALOG_FOR_NOT_TRANSLATED, true); + setUpLocaleConditions(false); mLocaleListEditor.onActivityResult(REQUEST_CONFIRM_SYSTEM_DEFAULT, Activity.RESULT_OK, mIntent); @@ -328,7 +369,7 @@ public class LocaleListEditorTest { Bundle bundle = new Bundle(); bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); mIntent.putExtras(bundle); - setUpLocaleConditions(); + setUpLocaleConditions(true); mLocaleListEditor.onActivityResult(REQUEST_CONFIRM_SYSTEM_DEFAULT, Activity.RESULT_CANCELED, mIntent); @@ -338,7 +379,7 @@ public class LocaleListEditorTest { @Test public void onTouch_dragDifferentLocaleToTop_showConfirmDialog() throws Exception { MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0); - setUpLocaleConditions(); + setUpLocaleConditions(true); final Configuration config = new Configuration(); config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US"))); when(mActivityService.getConfiguration()).thenReturn(config); @@ -352,7 +393,7 @@ public class LocaleListEditorTest { @Test public void onTouch_dragSameLocaleToTop_updateAdapter() throws Exception { MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0); - setUpLocaleConditions(); + setUpLocaleConditions(true); final Configuration config = new Configuration(); config.setLocales((LocaleList.forLanguageTags("en-US,zh-TW"))); when(mActivityService.getConfiguration()).thenReturn(config); @@ -490,12 +531,26 @@ public class LocaleListEditorTest { verify(mAdapter).setCheckBoxDescription(any(LocaleDragCell.class), any(), anyBoolean()); } - private void setUpLocaleConditions() { + private void setUpLocaleConditions(boolean isTranslated) { ShadowActivityManager.setService(mActivityService); mLocaleList = new ArrayList<>(); mLocaleList.add(mLocaleInfo); when(mLocaleInfo.getFullNameNative()).thenReturn("English"); when(mLocaleInfo.getLocale()).thenReturn(LocaleList.forLanguageTags("en-US").get(0)); + when(mLocaleInfo.isTranslated()).thenReturn(isTranslated); + when(mAdapter.getFeedItemList()).thenReturn(mLocaleList); + } + + private void setUpLocaleConditions2() { + ShadowActivityManager.setService(mActivityService); + mLocaleList = new ArrayList<>(); + mLocaleList.add(mLocaleInfo); + mLocaleList.add(mLocaleInfo1); + mLocaleList.add(mLocaleInfo2); + when(mLocaleInfo.getLocale()).thenReturn(Locale.forLanguageTag("blo-BJ")); + when(mLocaleInfo.isTranslated()).thenReturn(false); + when(mLocaleInfo2.getLocale()).thenReturn(Locale.forLanguageTag("zh-TW")); + when(mLocaleInfo2.isTranslated()).thenReturn(true); when(mAdapter.getFeedItemList()).thenReturn(mLocaleList); } } From c278a92940d8f31fe2995a9a9327c47bcf8ea784 Mon Sep 17 00:00:00 2001 From: Azhara Assanova Date: Mon, 24 Feb 2025 16:46:07 +0000 Subject: [PATCH 05/12] Update preference summary for ExternalSourcesDetails page when Advanced Protection is on The summary is now "Disabled". Bug: 398167869 Test: ExternalSourcesDetailsTest Flag: EXEMPT bug fix Change-Id: I1d8975671671c31b3176a39e07574ff623ed1a96 --- .../applications/appinfo/ExternalSourcesDetails.java | 3 +-- .../appinfo/ExternalSourcesDetailsTest.java | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java b/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java index 826583df866..ad1f823b48c 100644 --- a/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java +++ b/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java @@ -95,8 +95,7 @@ public class ExternalSourcesDetails extends AppInfoWithHeader userHandle)) { if (RestrictedLockUtilsInternal.isPolicyEnforcedByAdvancedProtection(context, DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, userHandle.getIdentifier())) { - return context.getString(com.android.settingslib.widget.restricted - .R.string.disabled_by_advanced_protection); + return context.getString(com.android.settingslib.R.string.disabled); } else { return context.getString( com.android.settingslib.widget.restricted.R.string.disabled_by_admin); diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/ExternalSourcesDetailsTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/ExternalSourcesDetailsTest.java index 65c9caf4302..7149d4f0c72 100644 --- a/tests/robotests/src/com/android/settings/applications/appinfo/ExternalSourcesDetailsTest.java +++ b/tests/robotests/src/com/android/settings/applications/appinfo/ExternalSourcesDetailsTest.java @@ -333,7 +333,7 @@ public class ExternalSourcesDetailsTest { @RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_FEATURE_DISABLE_INSTALL_UNKNOWN_SOURCES) @Test - public void getPreferenceSummary_restrictedGlobally_adminString() { + public void getPreferenceSummary_restrictedGloballyByAdmin_adminString() { final EnforcingAdmin nonAdvancedProtectionEnforcingAdmin = new EnforcingAdmin("test.pkg", UnknownAuthority.UNKNOWN_AUTHORITY, mUserHandle, new ComponentName("", "")); @@ -353,7 +353,7 @@ public class ExternalSourcesDetailsTest { @RequiresFlagsEnabled(Flags.FLAG_AAPM_FEATURE_DISABLE_INSTALL_UNKNOWN_SOURCES) @Test - public void getPreferenceSummary_restrictedGlobally_advancedProtectionString() { + public void getPreferenceSummary_restrictedGloballyByAdvancedProtection_disabledString() { final EnforcingAdmin advancedProtectionEnforcingAdmin = new EnforcingAdmin("test.pkg", new UnknownAuthority(ADVANCED_PROTECTION_SYSTEM_ENTITY), mUserHandle, new ComponentName("", "")); @@ -363,12 +363,10 @@ public class ExternalSourcesDetailsTest { advancedProtectionEnforcingAdmin); when(mUserManager.hasUserRestrictionForUser(DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, mUserHandle)).thenReturn(true); - when(mContext.getString( - com.android.settingslib.widget.restricted.R.string.disabled_by_advanced_protection)) - .thenReturn("disabled_by_advanced_protection"); + when(mContext.getString(com.android.settingslib.R.string.disabled)).thenReturn("disabled"); CharSequence summary = ExternalSourcesDetails.getPreferenceSummary(mContext, mAppEntry); - assertEquals("disabled_by_advanced_protection", summary.toString()); + assertEquals("disabled", summary.toString()); } } From a361eca7994d24b258acbb049951528966114217 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Wed, 5 Mar 2025 03:20:36 +0800 Subject: [PATCH 06/12] Fixed EditText Talkback issue in VPN Settings - Use Material3 TextInputLay & TextInputEditText to replace TextView & EditText Bug: 386014810 Flag: EXEMPT resource file only update Test: Manual testing Change-Id: I19ea8f64569173367c84e39fe9e892ecebdf2e2a --- res/layout/vpn_dialog.xml | 134 +++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/res/layout/vpn_dialog.xml b/res/layout/vpn_dialog.xml index 062772ee892..fadd2025f14 100644 --- a/res/layout/vpn_dialog.xml +++ b/res/layout/vpn_dialog.xml @@ -13,7 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - + + - - + + - - + + + - - + + android:inputType="textPassword"/> + - + android:id="@+id/vpn_proxy_host_layout" + android:hint="@string/proxy_hostname_label" + app:endIconMode="clear_text" + app:helperTextEnabled="true" + app:helperText="@string/proxy_hostname_hint" + app:errorEnabled="true"> + + - - - - - + android:id="@+id/vpn_proxy_port_layout" + android:hint="@string/proxy_port_label" + app:endIconMode="clear_text" + app:helperTextEnabled="true" + app:helperText="@string/proxy_port_hint" + app:errorEnabled="true"> + + @@ -182,18 +212,28 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - + + - - + + android:inputType="textPassword"/> + Date: Fri, 28 Feb 2025 11:26:23 -0800 Subject: [PATCH 07/12] Settings: Do not show install info for Play Store app Clicking the "App details" row in the info subpage for the Play Store app results in a crash. It appears that this app is not considered a mainline module and has an installer label, but this app is a special case. Hide the row for the Play Store app. Bug: 302093631 Test: atest com.android.settings.spa.app.appinfo.AppInstallerInfoPreferenceTest Flag: EXEMPT bugfix Change-Id: I291ee3e5dec2075381cb52d041982583cdd04323 --- .../app/appinfo/AppInstallerInfoPreference.kt | 13 +++++++++---- .../appinfo/AppInstallerInfoPreferenceTest.kt | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt b/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt index 7e160960efa..9bb3051efab 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreference.kt @@ -91,10 +91,15 @@ private class AppInstallerInfoPresenter( } }.sharedFlow() - val isAvailableFlow = installerLabelFlow.map { installerLabel -> - withContext(Dispatchers.IO) { - !AppUtils.isMainlineModule(packageManager, app.packageName) && - installerLabel != null + val isAvailableFlow = installerLabelFlow.map() { installerLabel -> + // Do not show the install info for the special case of the Play Store app. + if (app.packageName == context.getString(R.string.config_mainline_module_update_package)) { + false + } else { + withContext(Dispatchers.IO) { + val isMainlineModule = AppUtils.isMainlineModule(packageManager, app.packageName) + !isMainlineModule && installerLabel != null + } } } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt index 6297c62a015..52ee077c4d2 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt @@ -121,6 +121,25 @@ class AppInstallerInfoPreferenceTest { composeTestRule.onRoot().assertIsNotDisplayed() } + @Test + fun whenIsPlayStoreApp_notDisplayed() { + val playStorePackageName = "com.android.vending" + whenever( + AppStoreUtil.getInstallerPackageNameAndInstallSourceInfo( + any(), + eq(playStorePackageName) + ) + ) + .thenReturn(Pair(INSTALLER_PACKAGE_NAME, INSTALL_SOURCE_INFO)) + val playStoreApp = ApplicationInfo().apply { + packageName = playStorePackageName + uid = UID + } + setContent(playStoreApp) + + composeTestRule.onRoot().assertIsNotDisplayed() + } + @Test fun whenStoreLinkIsNull_disabled() { whenever(AppStoreUtil.getAppStoreLink(context, INSTALLER_PACKAGE_NAME, PACKAGE_NAME)) From c26eb7fd2442f9304c27108a7e0c32fa2da9e3af Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Thu, 27 Feb 2025 08:02:33 +0000 Subject: [PATCH 08/12] feat(A11yFeedback): Implements page ID to feedback bucket ID mapping This change adds a new API to a Pixel overlay feature provider, allowing Android to map page IDs to feedback bucket IDs. Bug: 393980229 Test: Manual testing for Pixel and non-Pixel overlay in real device Test: atest AccessibilitySettingsTest FeedbackManagerTest Flag: com.android.server.accessibility.enable_low_vision_generic_feedback Change-Id: I8a110b08816cac9c8a8e8c3e1218530fffb6f121 --- .../AccessibilityFeedbackFeatureProvider.java | 10 ++++------ ...AccessibilityFeedbackFeatureProviderImpl.java | 6 ++---- .../accessibility/AccessibilitySettings.java | 4 ++-- .../settings/accessibility/FeedbackManager.java | 16 +++------------- .../accessibility/AccessibilitySettingsTest.java | 13 ++----------- 5 files changed, 13 insertions(+), 36 deletions(-) diff --git a/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProvider.java b/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProvider.java index 018bd2e2ec1..7d03230ba96 100644 --- a/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProvider.java +++ b/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProvider.java @@ -15,8 +15,6 @@ */ package com.android.settings.accessibility; -import android.content.ComponentName; - import androidx.annotation.Nullable; /** @@ -25,11 +23,11 @@ import androidx.annotation.Nullable; public interface AccessibilityFeedbackFeatureProvider { /** - * Returns value according to the {@code componentName}. + * Returns value according to the {@code pageId}. * - * @param componentName the component name of the downloaded service or activity - * @return Feedback bucket ID + * @param pageId The unique identifier of the page. + * @return Feedback bucket ID associated with the page, or {@code null} if is not found. */ @Nullable - String getCategory(@Nullable ComponentName componentName); + String getCategory(int pageId); } diff --git a/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProviderImpl.java b/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProviderImpl.java index 917c5f64146..23818878327 100644 --- a/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProviderImpl.java +++ b/src/com/android/settings/accessibility/AccessibilityFeedbackFeatureProviderImpl.java @@ -15,8 +15,6 @@ */ package com.android.settings.accessibility; -import android.content.ComponentName; - import androidx.annotation.Nullable; /** Default implementation of {@link AccessibilityFeedbackFeatureProvider}. */ @@ -25,7 +23,7 @@ public class AccessibilityFeedbackFeatureProviderImpl implements @Override @Nullable - public String getCategory(@Nullable ComponentName componentName) { - return ""; + public String getCategory(int pageId) { + return null; } } diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 57eb4d5fba4..127906bf1c8 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -255,7 +255,7 @@ public class AccessibilitySettings extends DashboardFragment implements public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { if (getFeedbackManager().isAvailable()) { menu.add(Menu.NONE, MENU_ID_SEND_FEEDBACK, Menu.NONE, - getPrefContext().getText(R.string.accessibility_send_feedback_title)); + R.string.accessibility_send_feedback_title); } super.onCreateOptionsMenu(menu, inflater); } @@ -286,7 +286,7 @@ public class AccessibilitySettings extends DashboardFragment implements private FeedbackManager getFeedbackManager() { if (mFeedbackManager == null) { - mFeedbackManager = new FeedbackManager(getActivity()); + mFeedbackManager = new FeedbackManager(getActivity(), SettingsEnums.ACCESSIBILITY); } return mFeedbackManager; } diff --git a/src/com/android/settings/accessibility/FeedbackManager.java b/src/com/android/settings/accessibility/FeedbackManager.java index 52aefd22d31..dc4baa77d3b 100644 --- a/src/com/android/settings/accessibility/FeedbackManager.java +++ b/src/com/android/settings/accessibility/FeedbackManager.java @@ -16,7 +16,6 @@ package com.android.settings.accessibility; import android.app.Activity; -import android.content.ComponentName; import android.content.Intent; import android.text.TextUtils; @@ -46,23 +45,14 @@ public class FeedbackManager { * Constructs a new FeedbackManager. * * @param activity The activity context. A WeakReference is used to prevent memory leaks. + * @param pageId The unique identifier of the page associated with the feedback. */ - public FeedbackManager(@Nullable Activity activity) { - this(activity, /* componentName= */ null); - } - - /** - * Constructs a new FeedbackManager. - * - * @param activity The activity context. A WeakReference is used to prevent memory leaks. - * @param componentName The component name associated with the feedback. - */ - public FeedbackManager(@Nullable Activity activity, @Nullable ComponentName componentName) { + public FeedbackManager(@Nullable Activity activity, int pageId) { this(activity, DeviceInfoUtils.getFeedbackReporterPackage(activity), FeatureFactory.getFeatureFactory() .getAccessibilityFeedbackFeatureProvider() - .getCategory(componentName)); + .getCategory(pageId)); } /** diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java index e590a80b27d..6710da90788 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java @@ -22,7 +22,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -458,12 +457,10 @@ public class AccessibilitySettingsTest { setupFragment(); mFragment.setFeedbackManager( new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem); mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); - verify(mMenu).add(anyInt(), eq(AccessibilitySettings.MENU_ID_SEND_FEEDBACK), - anyInt(), eq(mContext.getText(R.string.accessibility_send_feedback_title))); + verify(mMenu).add(anyInt(), anyInt(), anyInt(), anyInt()); } @Test @@ -472,12 +469,10 @@ public class AccessibilitySettingsTest { setupFragment(); mFragment.setFeedbackManager( new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem); mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); - verify(mMenu, never()).add(anyInt(), eq(AccessibilitySettings.MENU_ID_SEND_FEEDBACK), - anyInt(), eq(mContext.getText(R.string.accessibility_send_feedback_title))); + verify(mMenu, never()).add(anyInt(), anyInt(), anyInt(), anyInt()); } @Test @@ -486,8 +481,6 @@ public class AccessibilitySettingsTest { setupFragment(); mFragment.setFeedbackManager( new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem); - mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); when(mMenuItem.getItemId()).thenReturn(AccessibilitySettings.MENU_ID_SEND_FEEDBACK); mFragment.onOptionsItemSelected(mMenuItem); @@ -502,8 +495,6 @@ public class AccessibilitySettingsTest { setupFragment(); mFragment.setFeedbackManager( new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem); - mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); when(mMenuItem.getItemId()).thenReturn(AccessibilitySettings.MENU_ID_SEND_FEEDBACK); mFragment.onOptionsItemSelected(mMenuItem); From bcddc529baf6695ee793ee293fa54e8214f4715d Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Tue, 4 Mar 2025 08:54:43 +0000 Subject: [PATCH 09/12] chore(A11yFeedback): Removing Legacy Android M Menu Behavior Removed legacy code that suppressed search and help menus for accessibility features. This workaround was implemented in Android M (ag/10438579) and is no longer needed. Bug: 393980229 Test: Manual testing on Pixel and non-Pixel devices Flag: EXEMPT cleanup Change-Id: Ide4978bb5d2fdacad48399fb8525c6a0dfcd4277 --- .../LaunchAccessibilityActivityPreferenceFragment.java | 9 --------- .../ToggleAccessibilityServicePreferenceFragment.java | 9 --------- 2 files changed, 18 deletions(-) diff --git a/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java b/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java index 013fdeeb215..3287ce8ec51 100644 --- a/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java +++ b/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java @@ -31,8 +31,6 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; @@ -115,13 +113,6 @@ public class LaunchAccessibilityActivityPreferenceFragment extends ToggleFeature return mTileComponentName; } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - // Do not call super. We don't want to see the "Help & feedback" option on this page so as - // not to confuse users who think they might be able to send feedback about a specific - // accessibility service from this page. - } - // IMPORTANT: Refresh the info since there are dynamically changing capabilities. private AccessibilityShortcutInfo getAccessibilityShortcutInfo() { final List infos = AccessibilityManager.getInstance( diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index 06bcdb7c2eb..3cd4bb6b3ed 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -39,8 +39,6 @@ import android.os.SystemClock; import android.text.BidiFormatter; import android.text.TextUtils; import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; import android.view.accessibility.AccessibilityManager; import android.widget.CompoundButton; @@ -75,13 +73,6 @@ public class ToggleAccessibilityServicePreferenceFragment extends return getArguments().getInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - // Do not call super. We don't want to see the "Help & feedback" option on this page so as - // not to confuse users who think they might be able to send feedback about a specific - // accessibility service from this page. - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); From 9ffca7cbfc4d077754ac711984229496313219bd Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Tue, 4 Mar 2025 09:20:33 +0000 Subject: [PATCH 10/12] feat(A11yFeedback): Add feedback entry for downloaded Accessibility This entry point allows users to access in the action bar. Visibility is controlled by the aconfig and FeedbackManager#isAvailable Bug: 393980229 Test: Manual testing on Pixel and non-Pixel devices Test: atest ToggleFeaturePreferenceFragmentTest Flag: com.android.server.accessibility.enable_low_vision_generic_feedback Change-Id: Ie6dfb6a887fe5a894622e86bab39878f8adea758 --- .../AccessibilityDetailsSettingsFragment.java | 1 + .../accessibility/AccessibilitySettings.java | 1 + ...cessibilityActivityPreferenceFragment.java | 5 ++ .../RestrictedPreferenceHelper.java | 1 + ...ccessibilityServicePreferenceFragment.java | 5 ++ .../ToggleFeaturePreferenceFragment.java | 48 +++++++++++++ ...eaderPreferenceFragmentForSetupWizard.java | 6 ++ ...SpeakPreferenceFragmentForSetupWizard.java | 6 ++ .../ToggleFeaturePreferenceFragmentTest.java | 72 ++++++++++++++++++- 9 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java index 4e9cd92d6c0..be7a73ba85a 100644 --- a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java @@ -232,6 +232,7 @@ public class AccessibilityDetailsSettingsFragment extends InstrumentedFragment { .getAccessibilityMetricsFeatureProvider() .getDownloadedFeatureMetricsCategory(componentName); extras.putInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY, metricsCategory); + extras.putInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY, metricsCategory); extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName); extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes()); diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 127906bf1c8..2c8247f959c 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -97,6 +97,7 @@ public class AccessibilitySettings extends DashboardFragment implements static final String EXTRA_HTML_DESCRIPTION = "html_description"; static final String EXTRA_TIME_FOR_LOGGING = "start_time_to_log_a11y_tool"; static final String EXTRA_METRICS_CATEGORY = "metrics_category"; + static final String EXTRA_FEEDBACK_CATEGORY = "feedback_category"; // Timeout before we update the services if packages are added/removed // since the AccessibilityManagerService has to do that processing first diff --git a/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java b/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java index 3287ce8ec51..c6995b01af7 100644 --- a/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java +++ b/src/com/android/settings/accessibility/LaunchAccessibilityActivityPreferenceFragment.java @@ -57,6 +57,11 @@ public class LaunchAccessibilityActivityPreferenceFragment extends ToggleFeature return getArguments().getInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY); } + @Override + public int getFeedbackCategory() { + return getArguments().getInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/src/com/android/settings/accessibility/RestrictedPreferenceHelper.java b/src/com/android/settings/accessibility/RestrictedPreferenceHelper.java index 5c18be8626c..ae6239af562 100644 --- a/src/com/android/settings/accessibility/RestrictedPreferenceHelper.java +++ b/src/com/android/settings/accessibility/RestrictedPreferenceHelper.java @@ -217,6 +217,7 @@ public class RestrictedPreferenceHelper { extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, imageRes); extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription); extras.putInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY, metricsCategory); + extras.putInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY, metricsCategory); } /** diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index 3cd4bb6b3ed..a11ad466003 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -73,6 +73,11 @@ public class ToggleAccessibilityServicePreferenceFragment extends return getArguments().getInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY); } + @Override + public int getFeedbackCategory() { + return getArguments().getInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index 93672516339..66c32df1798 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -40,6 +40,9 @@ import android.service.quicksettings.TileService; import android.text.Html; import android.text.TextUtils; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; @@ -48,6 +51,7 @@ import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -89,6 +93,7 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment // , a11y settings will get the resources successfully. private static final String IMG_PREFIX = "R.drawable."; private static final String DRAWABLE_FOLDER = "drawable"; + static final int MENU_ID_SEND_FEEDBACK = 0; protected TopIntroPreference mTopIntroPreference; protected SettingsMainSwitchPreference mToggleServiceSwitchPreference; @@ -102,6 +107,7 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment protected Intent mSettingsIntent; // The mComponentName maybe null, such as Magnify protected ComponentName mComponentName; + @Nullable private FeedbackManager mFeedbackManager; protected CharSequence mFeatureName; protected Uri mImageUri; protected CharSequence mHtmlDescription; @@ -240,6 +246,24 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment removeActionBarToggleSwitch(); } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + if (getFeedbackManager().isAvailable()) { + menu.add(Menu.NONE, MENU_ID_SEND_FEEDBACK, Menu.NONE, + R.string.accessibility_send_feedback_title); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == MENU_ID_SEND_FEEDBACK) { + getFeedbackManager().sendFeedback(); + return true; + } + return super.onOptionsItemSelected(item); + } + @Override public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { @@ -739,4 +763,28 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment super.onCreateRecyclerView(inflater, parent, savedInstanceState); return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView); } + + @VisibleForTesting + void setFeedbackManager(FeedbackManager feedbackManager) { + this.mFeedbackManager = feedbackManager; + } + + private FeedbackManager getFeedbackManager() { + if (mFeedbackManager == null) { + mFeedbackManager = new FeedbackManager(getActivity(), getFeedbackCategory()); + } + return mFeedbackManager; + } + + /** + * Returns the category of the feedback page. + * + *

By default, this method returns {@link SettingsEnums#PAGE_UNKNOWN}. This indicates that + * the feedback category is unknown, and the absence of a feedback menu. + * + * @return The feedback category, which is {@link SettingsEnums#PAGE_UNKNOWN} by default. + */ + protected int getFeedbackCategory() { + return SettingsEnums.PAGE_UNKNOWN; + } } diff --git a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java index 10813a7e262..eb0c93b755f 100644 --- a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java @@ -79,6 +79,12 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizard return SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER; } + @Override + public int getFeedbackCategory() { + // The feedback options should not be displayed on the setup wizard page. + return SettingsEnums.PAGE_UNKNOWN; + } + @Override public void onStop() { // Log the final choice in value if it's different from the previous value. diff --git a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java index 10796b5d218..14dc0bc1caf 100644 --- a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java @@ -79,6 +79,12 @@ public class ToggleSelectToSpeakPreferenceFragmentForSetupWizard return SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SELECT_TO_SPEAK; } + @Override + public int getFeedbackCategory() { + // The feedback options should not be displayed on the setup wizard page. + return SettingsEnums.PAGE_UNKNOWN; + } + @Override public void onStop() { // Log the final choice in value if it's different from the previous value. diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java index 8f9d2e1fbd0..571075cba31 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java @@ -23,9 +23,12 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,10 +40,13 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.icu.text.CaseMap; import android.os.Bundle; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; @@ -66,8 +72,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; @@ -83,6 +90,8 @@ import java.util.Locale; ShadowAccessibilityManager.class }) public class ToggleFeaturePreferenceFragmentTest { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -96,6 +105,7 @@ public class ToggleFeaturePreferenceFragmentTest { PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_TILE_CLASS_NAME); private static final String PLACEHOLDER_TILE_TOOLTIP_CONTENT = PLACEHOLDER_PACKAGE_NAME + "tooltip_content"; + private static final String PLACEHOLDER_CATEGORY = "category"; private static final String PLACEHOLDER_DIALOG_TITLE = "title"; private static final String DEFAULT_SUMMARY = "default summary"; private static final String DEFAULT_DESCRIPTION = "default description"; @@ -120,10 +130,13 @@ public class ToggleFeaturePreferenceFragmentTest { private ContentResolver mContentResolver; @Mock private PackageManager mPackageManager; + @Mock + private Menu mMenu; + @Mock + private MenuItem mMenuItem; @Before public void setUpTestFragment() { - MockitoAnnotations.initMocks(this); mShadowAccessibilityManager = Shadow.extract( mContext.getSystemService(AccessibilityManager.class)); @@ -169,6 +182,61 @@ public class ToggleFeaturePreferenceFragmentTest { any(AccessibilitySettingsContentObserver.class)); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() { + mFragment.setFeedbackManager( + new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); + + mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); + + verify(mMenu).add(anyInt(), eq(ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK), + anyInt(), eq(R.string.accessibility_send_feedback_title)); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() { + mFragment.setFeedbackManager( + new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); + + mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); + + verify(mMenu, never()).add(anyInt(), + eq(ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK), anyInt(), + eq(R.string.accessibility_send_feedback_title)); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() { + mFragment.setFeedbackManager( + new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); + when(mMenuItem.getItemId()).thenReturn( + ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK); + + mFragment.onOptionsItemSelected(mMenuItem); + + verify(mActivity).startActivityForResult( + argThat(intent -> intent != null + && Intent.ACTION_BUG_REPORT.equals(intent.getAction())), anyInt()); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() { + mFragment.setFeedbackManager( + new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); + when(mMenuItem.getItemId()).thenReturn( + ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK); + + mFragment.onOptionsItemSelected(mMenuItem); + + verify(mActivity, never()).startActivityForResult( + argThat(intent -> intent != null + && Intent.ACTION_BUG_REPORT.equals(intent.getAction())), anyInt()); + } + @Test public void updateShortcutPreferenceData_assignDefaultValueToVariable() { mFragment.mComponentName = PLACEHOLDER_COMPONENT_NAME; From b511fe690bcf3a051e8149655bbc422777cfc6f1 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Mon, 3 Mar 2025 02:58:50 +0000 Subject: [PATCH 11/12] refactor(A11yFeedback): Rename AccessibilityMetricsFeatureProvider The AccessibilityMetricsFeatureProvider currently provides page IDs derived from component names, primarily for metrics purposes. However, its functionality should be expanded to also support feedback needs, not just metrics collection, by returning appropriate settings enum values. Bug: 393980229 Test: atest AccessibilityMetricsFeatureProviderGoogleImplTest Flag: com.android.server.accessibility.enable_low_vision_generic_feedback Change-Id: I2d64db866010ae5e3c6a9738f92860df3d0b86aa --- .../accessibility/AccessibilityActivityPreference.java | 7 +++---- .../AccessibilityDetailsSettingsFragment.java | 9 ++++----- ...er.java => AccessibilityPageIdFeatureProvider.java} | 6 +++--- ...ava => AccessibilityPageIdFeatureProviderImpl.java} | 10 ++++++---- .../accessibility/AccessibilityServicePreference.java | 7 +++---- src/com/android/settings/overlay/FeatureFactory.kt | 6 +++--- src/com/android/settings/overlay/FeatureFactoryImpl.kt | 8 ++++---- .../android/settings/testutils/FakeFeatureFactory.java | 10 +++++----- .../android/settings/testutils/FakeFeatureFactory.kt | 4 ++-- .../android/settings/testutils/FakeFeatureFactory.java | 10 +++++----- 10 files changed, 38 insertions(+), 39 deletions(-) rename src/com/android/settings/accessibility/{AccessibilityMetricsFeatureProvider.java => AccessibilityPageIdFeatureProvider.java} (84%) rename src/com/android/settings/accessibility/{AccessibilityMetricsFeatureProviderImpl.java => AccessibilityPageIdFeatureProviderImpl.java} (75%) diff --git a/src/com/android/settings/accessibility/AccessibilityActivityPreference.java b/src/com/android/settings/accessibility/AccessibilityActivityPreference.java index a8e456d3e35..3ae64fcb873 100644 --- a/src/com/android/settings/accessibility/AccessibilityActivityPreference.java +++ b/src/com/android/settings/accessibility/AccessibilityActivityPreference.java @@ -127,14 +127,13 @@ public class AccessibilityActivityPreference extends RestrictedPreference { final String htmlDescription = mA11yShortcutInfo.loadHtmlDescription(mPm); final String settingsClassName = mA11yShortcutInfo.getSettingsActivityName(); final String tileServiceClassName = mA11yShortcutInfo.getTileServiceName(); - final int metricsCategory = FeatureFactory.getFeatureFactory() - .getAccessibilityMetricsFeatureProvider() - .getDownloadedFeatureMetricsCategory(mComponentName); + final int pageIdCategory = FeatureFactory.getFeatureFactory() + .getAccessibilityPageIdFeatureProvider().getCategory(mComponentName); ThreadUtils.getUiThreadHandler().post(() -> { RestrictedPreferenceHelper.putBasicExtras( this, prefKey, getTitle(), intro, description, imageRes, - htmlDescription, mComponentName, metricsCategory); + htmlDescription, mComponentName, pageIdCategory); RestrictedPreferenceHelper.putSettingsExtras(this, getPackageName(), settingsClassName); RestrictedPreferenceHelper.putTileServiceExtras( this, getPackageName(), tileServiceClassName); diff --git a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java index be7a73ba85a..347c7693356 100644 --- a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java @@ -228,11 +228,10 @@ public class AccessibilityDetailsSettingsFragment extends InstrumentedFragment { new ComponentName(packageName, tileServiceClassName).flattenToString()); } - final int metricsCategory = FeatureFactory.getFeatureFactory() - .getAccessibilityMetricsFeatureProvider() - .getDownloadedFeatureMetricsCategory(componentName); - extras.putInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY, metricsCategory); - extras.putInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY, metricsCategory); + final int pageIdCategory = FeatureFactory.getFeatureFactory() + .getAccessibilityPageIdFeatureProvider().getCategory(componentName); + extras.putInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY, pageIdCategory); + extras.putInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY, pageIdCategory); extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName); extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes()); diff --git a/src/com/android/settings/accessibility/AccessibilityMetricsFeatureProvider.java b/src/com/android/settings/accessibility/AccessibilityPageIdFeatureProvider.java similarity index 84% rename from src/com/android/settings/accessibility/AccessibilityMetricsFeatureProvider.java rename to src/com/android/settings/accessibility/AccessibilityPageIdFeatureProvider.java index a9d7c0551d1..698efbeee90 100644 --- a/src/com/android/settings/accessibility/AccessibilityMetricsFeatureProvider.java +++ b/src/com/android/settings/accessibility/AccessibilityPageIdFeatureProvider.java @@ -21,9 +21,9 @@ import android.content.ComponentName; import androidx.annotation.Nullable; /** - * Provider for Accessibility metrics related features. + * Provider for Accessibility page id related features. */ -public interface AccessibilityMetricsFeatureProvider { +public interface AccessibilityPageIdFeatureProvider { /** * Returns {@link android.app.settings.SettingsEnums} value according to the {@code @@ -32,5 +32,5 @@ public interface AccessibilityMetricsFeatureProvider { * @param componentName the component name of the downloaded service or activity * @return value in {@link android.app.settings.SettingsEnums} */ - int getDownloadedFeatureMetricsCategory(@Nullable ComponentName componentName); + int getCategory(@Nullable ComponentName componentName); } diff --git a/src/com/android/settings/accessibility/AccessibilityMetricsFeatureProviderImpl.java b/src/com/android/settings/accessibility/AccessibilityPageIdFeatureProviderImpl.java similarity index 75% rename from src/com/android/settings/accessibility/AccessibilityMetricsFeatureProviderImpl.java rename to src/com/android/settings/accessibility/AccessibilityPageIdFeatureProviderImpl.java index 0f85f38f571..acd8aab3291 100644 --- a/src/com/android/settings/accessibility/AccessibilityMetricsFeatureProviderImpl.java +++ b/src/com/android/settings/accessibility/AccessibilityPageIdFeatureProviderImpl.java @@ -19,14 +19,16 @@ package com.android.settings.accessibility; import android.app.settings.SettingsEnums; import android.content.ComponentName; +import androidx.annotation.Nullable; + /** - * Provider implementation for Accessibility metrics related features. + * Provider implementation for Accessibility page id related features. */ -public class AccessibilityMetricsFeatureProviderImpl implements - AccessibilityMetricsFeatureProvider { +public class AccessibilityPageIdFeatureProviderImpl implements + AccessibilityPageIdFeatureProvider { @Override - public int getDownloadedFeatureMetricsCategory(ComponentName componentName) { + public int getCategory(@Nullable ComponentName componentName) { return SettingsEnums.ACCESSIBILITY_SERVICE; } } diff --git a/src/com/android/settings/accessibility/AccessibilityServicePreference.java b/src/com/android/settings/accessibility/AccessibilityServicePreference.java index 8a22d820af9..703277422d6 100644 --- a/src/com/android/settings/accessibility/AccessibilityServicePreference.java +++ b/src/com/android/settings/accessibility/AccessibilityServicePreference.java @@ -123,13 +123,12 @@ public class AccessibilityServicePreference extends RestrictedPreference { final String settingsClassName = mA11yServiceInfo.getSettingsActivityName(); final String tileServiceClassName = mA11yServiceInfo.getTileServiceName(); final ResolveInfo resolveInfo = mA11yServiceInfo.getResolveInfo(); - final int metricsCategory = FeatureFactory.getFeatureFactory() - .getAccessibilityMetricsFeatureProvider() - .getDownloadedFeatureMetricsCategory(mComponentName); + final int pageIdCategory = FeatureFactory.getFeatureFactory() + .getAccessibilityPageIdFeatureProvider().getCategory(mComponentName); ThreadUtils.getUiThreadHandler().post(() -> { RestrictedPreferenceHelper.putBasicExtras( this, prefKey, getTitle(), intro, description, imageRes, - htmlDescription, mComponentName, metricsCategory); + htmlDescription, mComponentName, pageIdCategory); RestrictedPreferenceHelper.putServiceExtras(this, resolveInfo, mServiceEnabled); RestrictedPreferenceHelper.putSettingsExtras(this, getPackageName(), settingsClassName); RestrictedPreferenceHelper.putTileServiceExtras( diff --git a/src/com/android/settings/overlay/FeatureFactory.kt b/src/com/android/settings/overlay/FeatureFactory.kt index 46aa19b0d05..7e04f0d4373 100644 --- a/src/com/android/settings/overlay/FeatureFactory.kt +++ b/src/com/android/settings/overlay/FeatureFactory.kt @@ -17,7 +17,7 @@ package com.android.settings.overlay import android.content.Context import com.android.settings.accessibility.AccessibilityFeedbackFeatureProvider -import com.android.settings.accessibility.AccessibilityMetricsFeatureProvider +import com.android.settings.accessibility.AccessibilityPageIdFeatureProvider import com.android.settings.accessibility.AccessibilitySearchFeatureProvider import com.android.settings.accounts.AccountFeatureProvider import com.android.settings.applications.ApplicationFeatureProvider @@ -145,9 +145,9 @@ abstract class FeatureFactory { abstract val accessibilitySearchFeatureProvider: AccessibilitySearchFeatureProvider /** - * Retrieves implementation for Accessibility metrics category feature. + * Retrieves implementation for Accessibility page id category feature. */ - abstract val accessibilityMetricsFeatureProvider: AccessibilityMetricsFeatureProvider + abstract val accessibilityPageIdFeatureProvider: AccessibilityPageIdFeatureProvider /** * Retrieves implementation for advanced vpn feature. diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.kt b/src/com/android/settings/overlay/FeatureFactoryImpl.kt index 08abf2bd466..4949c3f7f0c 100644 --- a/src/com/android/settings/overlay/FeatureFactoryImpl.kt +++ b/src/com/android/settings/overlay/FeatureFactoryImpl.kt @@ -22,8 +22,8 @@ import android.net.VpnManager import android.os.UserManager import com.android.settings.accessibility.AccessibilityFeedbackFeatureProvider import com.android.settings.accessibility.AccessibilityFeedbackFeatureProviderImpl -import com.android.settings.accessibility.AccessibilityMetricsFeatureProvider -import com.android.settings.accessibility.AccessibilityMetricsFeatureProviderImpl +import com.android.settings.accessibility.AccessibilityPageIdFeatureProvider +import com.android.settings.accessibility.AccessibilityPageIdFeatureProviderImpl import com.android.settings.accessibility.AccessibilitySearchFeatureProvider import com.android.settings.accessibility.AccessibilitySearchFeatureProviderImpl import com.android.settings.accounts.AccountFeatureProvider @@ -174,8 +174,8 @@ open class FeatureFactoryImpl : FeatureFactory() { AccessibilitySearchFeatureProviderImpl() } - override val accessibilityMetricsFeatureProvider: AccessibilityMetricsFeatureProvider by lazy { - AccessibilityMetricsFeatureProviderImpl() + override val accessibilityPageIdFeatureProvider: AccessibilityPageIdFeatureProvider by lazy { + AccessibilityPageIdFeatureProviderImpl() } override val advancedVpnFeatureProvider by lazy { AdvancedVpnFeatureProviderImpl() } diff --git a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java index c5d4c36228a..e002de1e391 100644 --- a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.mock; import android.content.Context; import com.android.settings.accessibility.AccessibilityFeedbackFeatureProvider; -import com.android.settings.accessibility.AccessibilityMetricsFeatureProvider; +import com.android.settings.accessibility.AccessibilityPageIdFeatureProvider; import com.android.settings.accessibility.AccessibilitySearchFeatureProvider; import com.android.settings.accounts.AccountFeatureProvider; import com.android.settings.applications.ApplicationFeatureProvider; @@ -94,7 +94,7 @@ public class FakeFeatureFactory extends FeatureFactory { public WifiTrackerLibProvider wifiTrackerLibProvider; public SecuritySettingsFeatureProvider securitySettingsFeatureProvider; public AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider; - public AccessibilityMetricsFeatureProvider mAccessibilityMetricsFeatureProvider; + public AccessibilityPageIdFeatureProvider mAccessibilityPageIdFeatureProvider; public AdvancedVpnFeatureProvider mAdvancedVpnFeatureProvider; public WifiFeatureProvider mWifiFeatureProvider; public KeyboardSettingsFeatureProvider mKeyboardSettingsFeatureProvider; @@ -145,7 +145,7 @@ public class FakeFeatureFactory extends FeatureFactory { wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class); securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class); mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class); - mAccessibilityMetricsFeatureProvider = mock(AccessibilityMetricsFeatureProvider.class); + mAccessibilityPageIdFeatureProvider = mock(AccessibilityPageIdFeatureProvider.class); mAdvancedVpnFeatureProvider = mock(AdvancedVpnFeatureProvider.class); mWifiFeatureProvider = mock(WifiFeatureProvider.class); mKeyboardSettingsFeatureProvider = mock(KeyboardSettingsFeatureProvider.class); @@ -294,8 +294,8 @@ public class FakeFeatureFactory extends FeatureFactory { } @Override - public AccessibilityMetricsFeatureProvider getAccessibilityMetricsFeatureProvider() { - return mAccessibilityMetricsFeatureProvider; + public AccessibilityPageIdFeatureProvider getAccessibilityPageIdFeatureProvider() { + return mAccessibilityPageIdFeatureProvider; } @Override diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt index 56dd444b474..7b1bdc0ed9c 100644 --- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt +++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt @@ -18,7 +18,7 @@ package com.android.settings.testutils import android.content.Context import com.android.settings.accessibility.AccessibilityFeedbackFeatureProvider -import com.android.settings.accessibility.AccessibilityMetricsFeatureProvider +import com.android.settings.accessibility.AccessibilityPageIdFeatureProvider import com.android.settings.accessibility.AccessibilitySearchFeatureProvider import com.android.settings.accounts.AccountFeatureProvider import com.android.settings.applications.ApplicationFeatureProvider @@ -130,7 +130,7 @@ class FakeFeatureFactory : FeatureFactory() { get() = TODO("Not yet implemented") override val accessibilitySearchFeatureProvider: AccessibilitySearchFeatureProvider get() = TODO("Not yet implemented") - override val accessibilityMetricsFeatureProvider: AccessibilityMetricsFeatureProvider + override val accessibilityPageIdFeatureProvider: AccessibilityPageIdFeatureProvider get() = TODO("Not yet implemented") override val advancedVpnFeatureProvider: AdvancedVpnFeatureProvider get() = TODO("Not yet implemented") diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java index d77d7a4ff01..eda0aeb934c 100644 --- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.mock; import android.content.Context; import com.android.settings.accessibility.AccessibilityFeedbackFeatureProvider; -import com.android.settings.accessibility.AccessibilityMetricsFeatureProvider; +import com.android.settings.accessibility.AccessibilityPageIdFeatureProvider; import com.android.settings.accessibility.AccessibilitySearchFeatureProvider; import com.android.settings.accounts.AccountFeatureProvider; import com.android.settings.applications.ApplicationFeatureProvider; @@ -93,7 +93,7 @@ public class FakeFeatureFactory extends FeatureFactory { public WifiTrackerLibProvider wifiTrackerLibProvider; public SecuritySettingsFeatureProvider securitySettingsFeatureProvider; public AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider; - public AccessibilityMetricsFeatureProvider mAccessibilityMetricsFeatureProvider; + public AccessibilityPageIdFeatureProvider mAccessibilityPageIdFeatureProvider; public AdvancedVpnFeatureProvider mAdvancedVpnFeatureProvider; public WifiFeatureProvider mWifiFeatureProvider; public KeyboardSettingsFeatureProvider mKeyboardSettingsFeatureProvider; @@ -146,7 +146,7 @@ public class FakeFeatureFactory extends FeatureFactory { wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class); securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class); mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class); - mAccessibilityMetricsFeatureProvider = mock(AccessibilityMetricsFeatureProvider.class); + mAccessibilityPageIdFeatureProvider = mock(AccessibilityPageIdFeatureProvider.class); mAdvancedVpnFeatureProvider = mock(AdvancedVpnFeatureProvider.class); mWifiFeatureProvider = mock(WifiFeatureProvider.class); mKeyboardSettingsFeatureProvider = mock(KeyboardSettingsFeatureProvider.class); @@ -295,8 +295,8 @@ public class FakeFeatureFactory extends FeatureFactory { } @Override - public AccessibilityMetricsFeatureProvider getAccessibilityMetricsFeatureProvider() { - return mAccessibilityMetricsFeatureProvider; + public AccessibilityPageIdFeatureProvider getAccessibilityPageIdFeatureProvider() { + return mAccessibilityPageIdFeatureProvider; } @Override From f7a128496cbd6fa3156871a7949de39fd0f91118 Mon Sep 17 00:00:00 2001 From: Chun Zhang Date: Mon, 3 Feb 2025 18:16:23 +0000 Subject: [PATCH 12/12] Add WearSafetySource (Without Listener) Bug: 389841524 Test: TreeHugger Test: manual Test: atest SafetySourceBroadcastReceiverTest Test: atest LockScreenSafetySourceTest Test: atest ActiveUnlockStatusUtilsTest Test: atest WearSafetySourceTest Flag: com.android.settings.flags.biometrics_onboarding_education Change-Id: I7f4b41bf33d1e0fb7988f756a466e4d80bcec25e --- res/values/strings.xml | 1 + .../ActiveUnlockContentListener.java | 63 ++-- .../ActiveUnlockDeviceNameListener.java | 4 +- .../activeunlock/ActiveUnlockStatusUtils.java | 33 +- .../ActiveUnlockSummaryListener.java | 4 +- .../combination/BiometricsSettingsBase.java | 2 +- .../safetycenter/LockScreenSafetySource.java | 1 + .../SafetySourceBroadcastReceiver.java | 4 + .../safetycenter/WearSafetySource.java | 147 +++++++++ .../ActiveUnlockStatusUtilsTest.java | 7 + .../LockScreenSafetySourceTest.java | 3 + .../SafetySourceBroadcastReceiverTest.java | 26 +- .../safetycenter/WearSafetySourceTest.java | 295 ++++++++++++++++++ 13 files changed, 562 insertions(+), 28 deletions(-) create mode 100644 src/com/android/settings/safetycenter/WearSafetySource.java create mode 100644 tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index cbe56401774..60aab52ec6f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1054,6 +1054,7 @@ Watch Unlock + Watch When you set up Face Unlock and Fingerprint Unlock, your phone will ask for your fingerprint when you wear a mask or are in a dark area.\n\nWatch Unlock is another convenient way to unlock your phone, for example, when your fingers are wet or face isn\u2019t recognized. diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java index 8cc7d6af331..42029ff89f1 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java @@ -28,6 +28,7 @@ import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.settingslib.utils.ThreadUtils; @@ -76,17 +77,22 @@ public class ActiveUnlockContentListener { mContentKey = contentKey; String authority = new ActiveUnlockStatusUtils(mContext).getAuthority(); if (authority != null) { - mUri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(authority) - .appendPath(CONTENT_PROVIDER_PATH) - .build(); + mUri = getUri(authority); } else { mUri = null; } } + /** Returns Active Unlock Uri. */ + public static @NonNull Uri getUri(@NonNull String authority) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(CONTENT_PROVIDER_PATH) + .build(); + } + /** Returns true if start listening for updates from the ContentProvider, false otherwise. */ public synchronized boolean subscribe() { if (mSubscribed || mUri == null) { @@ -123,25 +129,40 @@ public class ActiveUnlockContentListener { Log.e(mLogTag, "Uri null when trying to fetch content"); return; } - ContentResolver contentResolver = mContext.getContentResolver(); - ContentProviderClient client = contentResolver.acquireContentProviderClient(mUri); - Bundle bundle; - try { - bundle = client.call(mMethodName, null /* arg */, null /* extras */); - } catch (RemoteException e) { - Log.e(mLogTag, "Failed to call contentProvider", e); - return; - } finally { - client.close(); - } - if (bundle == null) { - Log.e(mLogTag, "Null bundle returned from contentProvider"); - return; - } - String newValue = bundle.getString(mContentKey); + + @Nullable String newValue = getContentFromUri( + mContext, mUri, mLogTag, mMethodName, mContentKey); if (!TextUtils.equals(mContent, newValue)) { mContent = newValue; mContentChangedListener.onContentChanged(mContent); } } + + /** Get the content from Uri. */ + public static @Nullable String getContentFromUri( + @NonNull Context context, + @NonNull Uri uri, + @NonNull String logTag, + @NonNull String methodName, + @NonNull String contentKey) { + ContentResolver contentResolver = context.getContentResolver(); + ContentProviderClient client = contentResolver.acquireContentProviderClient(uri); + + @Nullable Bundle bundle = null; + + try { + bundle = client.call(methodName, /* arg= */ null, /* extras = */ null); + } catch (RemoteException e) { + Log.e(logTag, "Failed to call contentProvider", e); + } finally { + client.close(); + } + + if (bundle == null) { + Log.e(logTag, "Null bundle returned from contentProvider"); + return null; + } + + return bundle.getString(contentKey); + } } diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java index 1badb0f26ec..9e8176294e5 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java @@ -21,8 +21,8 @@ import android.content.Context; /** Listens to device name updates from the content provider and fetches the latest value. */ public class ActiveUnlockDeviceNameListener { private static final String TAG = "ActiveUnlockDeviceNameListener"; - private static final String METHOD_NAME = "getDeviceName"; - private static final String DEVICE_NAME_KEY = "com.android.settings.active_unlock.device_name"; + static final String METHOD_NAME = "getDeviceName"; + static final String DEVICE_NAME_KEY = "com.android.settings.active_unlock.device_name"; private final ActiveUnlockContentListener mActiveUnlockContentListener; public ActiveUnlockDeviceNameListener( diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java index 4ff2e900ae2..66485d37601 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java @@ -155,10 +155,17 @@ public class ActiveUnlockStatusUtils { return BasePreferenceController.CONDITIONALLY_UNAVAILABLE; } + /** + * Returns the title of active unlock only. + */ + public @NonNull String getTitleForActiveUnlockOnly() { + return mContext.getString(R.string.security_settings_activeunlock); + } + /** * Returns the title of the combined biometric settings entity when active unlock is enabled. */ - public String getTitleForActiveUnlock() { + public @NonNull String getTitleForActiveUnlock() { final boolean faceAllowed = Utils.hasFaceHardware(mContext); final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext); return mContext.getString(getTitleRes(faceAllowed, fingerprintAllowed)); @@ -264,6 +271,30 @@ public class ActiveUnlockStatusUtils { return mContext.getString(getUseBiometricTitleRes(faceAllowed, fingerprintAllowed)); } + /** + * Returns the summary from content provider. + */ + @Nullable + public static String getSummaryFromContentProvider( + @NonNull Context context, @NonNull String authority, @NonNull String logTag) { + return ActiveUnlockContentListener.getContentFromUri( + context, ActiveUnlockContentListener.getUri(authority), logTag, + ActiveUnlockSummaryListener.METHOD_NAME, + ActiveUnlockSummaryListener.SUMMARY_KEY); + } + + /** + * Returns the device name from content provider. + */ + @Nullable + public static String getDeviceNameFromContentProvider( + @NonNull Context context, @NonNull String authority, @NonNull String logTag) { + return ActiveUnlockContentListener.getContentFromUri( + context, ActiveUnlockContentListener.getUri(authority), logTag, + ActiveUnlockDeviceNameListener.METHOD_NAME, + ActiveUnlockDeviceNameListener.DEVICE_NAME_KEY); + } + @StringRes private static int getUseBiometricTitleRes( boolean isFaceAllowed, boolean isFingerprintAllowed) { diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java index bcffe6297d1..38e137bd379 100644 --- a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java +++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java @@ -21,8 +21,8 @@ import android.content.Context; /** Listens to summary updates from the content provider and fetches the latest value. */ public class ActiveUnlockSummaryListener { private static final String TAG = "ActiveUnlockSummaryListener"; - private static final String METHOD_NAME = "getSummary"; - private static final String SUMMARY_KEY = "com.android.settings.summary"; + static final String METHOD_NAME = "getSummary"; + static final String SUMMARY_KEY = "com.android.settings.summary"; private final ActiveUnlockContentListener mContentListener; public ActiveUnlockSummaryListener( diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index 1d8b7a10a3c..ed4b713df20 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -66,7 +66,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @VisibleForTesting static final int CONFIRM_REQUEST = 2001; private static final int CHOOSE_LOCK_REQUEST = 2002; - protected static final int ACTIVE_UNLOCK_REQUEST = 2003; + public static final int ACTIVE_UNLOCK_REQUEST = 2003; @VisibleForTesting static final int BIOMETRIC_AUTH_REQUEST = 2004; diff --git a/src/com/android/settings/safetycenter/LockScreenSafetySource.java b/src/com/android/settings/safetycenter/LockScreenSafetySource.java index 00a4c676a80..61f05f7f02b 100644 --- a/src/com/android/settings/safetycenter/LockScreenSafetySource.java +++ b/src/com/android/settings/safetycenter/LockScreenSafetySource.java @@ -131,6 +131,7 @@ public final class LockScreenSafetySource { if (Flags.biometricsOnboardingEducation()) { FaceSafetySource.onBiometricsChanged(context); FingerprintSafetySource.onBiometricsChanged(context); + WearSafetySource.onBiometricsChanged(context); } else { BiometricsSafetySource.onBiometricsChanged(context); } diff --git a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java index a49b7e0f860..4cf40ddbdbe 100644 --- a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java +++ b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java @@ -86,6 +86,9 @@ public class SafetySourceBroadcastReceiver extends BroadcastReceiver { if (sourceIds.contains(FingerprintSafetySource.SAFETY_SOURCE_ID)) { FingerprintSafetySource.setSafetySourceData(context, safetyEvent); } + if (sourceIds.contains(WearSafetySource.SAFETY_SOURCE_ID)) { + WearSafetySource.setSafetySourceData(context, safetyEvent); + } } private static void refreshAllSafetySources(Context context, SafetyEvent safetyEvent) { @@ -95,5 +98,6 @@ public class SafetySourceBroadcastReceiver extends BroadcastReceiver { PrivateSpaceSafetySource.setSafetySourceData(context, safetyEvent); FaceSafetySource.setSafetySourceData(context, safetyEvent); FingerprintSafetySource.setSafetySourceData(context, safetyEvent); + WearSafetySource.setSafetySourceData(context, safetyEvent); } } diff --git a/src/com/android/settings/safetycenter/WearSafetySource.java b/src/com/android/settings/safetycenter/WearSafetySource.java new file mode 100644 index 00000000000..a345096728b --- /dev/null +++ b/src/com/android/settings/safetycenter/WearSafetySource.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2025 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.safetycenter; + +import static com.android.settings.biometrics.combination.BiometricsSettingsBase.ACTIVE_UNLOCK_REQUEST; + +import android.app.PendingIntent; +import android.content.Context; +import android.os.UserManager; +import android.safetycenter.SafetyEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; +import com.android.settings.flags.Flags; + +/** Wear Safety Source for Safety Center. */ +public final class WearSafetySource { + + private static final String TAG = "WearSafetySource"; + public static final String SAFETY_SOURCE_ID = "AndroidWearUnlock"; + private static boolean sIsTestingEnv = false; + private static String sSummaryForTesting = ""; + private static boolean sHasEnrolledForTesting; + + private WearSafetySource() {} + + /** Sets test value for summary. */ + @VisibleForTesting + public static void setSummaryForTesting(@NonNull String summary) { + sIsTestingEnv = true; + sSummaryForTesting = summary; + } + + /** Sets test value for hasEnrolled. */ + @VisibleForTesting + public static void setHasEnrolledForTesting(boolean hasEnrolled) { + sIsTestingEnv = true; + sHasEnrolledForTesting = hasEnrolled; + } + + /** Sets biometric safety data for Safety Center. */ + public static void setSafetySourceData( + @NonNull Context context, @NonNull SafetyEvent safetyEvent) { + if (!SafetyCenterManagerWrapper.get().isEnabled(context)) { + return; + } + if (!Flags.biometricsOnboardingEducation()) { // this source is effectively turned off + sendNullData(context, safetyEvent); + return; + } + + // Handle private profile case. + UserManager userManager = UserManager.get(context); + if (android.os.Flags.allowPrivateProfile() + && android.multiuser.Flags.enablePrivateSpaceFeatures() + && userManager.isPrivateProfile()) { + // SC always expects a response from the source if the broadcast has been sent for this + // source, therefore, we need to send a null SafetySourceData. + sendNullData(context, safetyEvent); + return; + } + + ActiveUnlockStatusUtils activeUnlockStatusUtils = new ActiveUnlockStatusUtils(context); + if (!userManager.isProfile() && activeUnlockStatusUtils.isAvailable()) { + boolean hasEnrolled = false; + String summary = ""; + + if (sIsTestingEnv) { + hasEnrolled = sHasEnrolledForTesting; + summary = sSummaryForTesting; + } else { + String authority = new ActiveUnlockStatusUtils(context).getAuthority(); + hasEnrolled = getHasEnrolledFromContentProvider(context, authority); + summary = getSummaryFromContentProvider(context, authority); + } + + BiometricSourcesUtils.setBiometricSafetySourceData( + SAFETY_SOURCE_ID, + context, + activeUnlockStatusUtils.getTitleForActiveUnlockOnly(), + summary, + PendingIntent.getActivity(context, ACTIVE_UNLOCK_REQUEST, + activeUnlockStatusUtils.getIntent(), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT), + /* enabled= */ true, + hasEnrolled, + safetyEvent); + return; + } + + sendNullData(context, safetyEvent); + } + + private static void sendNullData(Context context, SafetyEvent safetyEvent) { + SafetyCenterManagerWrapper.get() + .setSafetySourceData( + context, SAFETY_SOURCE_ID, /* safetySourceData= */ null, safetyEvent); + } + + /** Notifies Safety Center of a change in wear biometrics settings. */ + public static void onBiometricsChanged(@NonNull Context context) { + setSafetySourceData( + context, + new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED) + .build()); + } + + private static boolean getHasEnrolledFromContentProvider( + @NonNull Context context, @Nullable String authority) { + if (authority == null) { + return false; + } + return ActiveUnlockStatusUtils.getDeviceNameFromContentProvider(context, authority, TAG) + != null; + } + + private static String getSummaryFromContentProvider( + @NonNull Context context, @Nullable String authority) { + if (authority == null) { + return ""; + } + String summary = ActiveUnlockStatusUtils.getSummaryFromContentProvider( + context, authority, TAG); + if (summary == null) { + return ""; + } + return summary; + } + +} diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java index 563974d5287..6da6aa78103 100644 --- a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java @@ -243,4 +243,11 @@ public class ActiveUnlockStatusUtilsTest { .isEqualTo(mApplicationContext.getString( R.string.biometric_settings_use_watch_for)); } + + @Test + public void getTitleForActiveUnlockOnly_returnsTile() { + assertThat(mActiveUnlockStatusUtils.getTitleForActiveUnlockOnly()) + .isEqualTo(mApplicationContext.getString( + R.string.security_settings_activeunlock)); + } } diff --git a/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java index f16113ab13d..6e46d2be551 100644 --- a/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/LockScreenSafetySourceTest.java @@ -527,6 +527,9 @@ public class LockScreenSafetySourceTest { verify(mSafetyCenterManagerWrapper) .setSafetySourceData( any(), eq(FingerprintSafetySource.SAFETY_SOURCE_ID), any(), any()); + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), eq(WearSafetySource.SAFETY_SOURCE_ID), any(), any()); } @Test diff --git a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java index e65d041e248..836247c38c5 100644 --- a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java @@ -245,6 +245,25 @@ public class SafetySourceBroadcastReceiverTest { assertThat(captor.getValue()).isEqualTo(FaceSafetySource.SAFETY_SOURCE_ID); } + @Test + public void onReceive_onRefresh_withWearUnlockSourceId_setsWearUnlockData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent() + .setAction(ACTION_REFRESH_SAFETY_SOURCES) + .putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[] {WearSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(mSafetyCenterManagerWrapper, times(1)) + .setSafetySourceData(any(), captor.capture(), any(), any()); + + assertThat(captor.getValue()).isEqualTo(WearSafetySource.SAFETY_SOURCE_ID); + } + @Test public void onReceive_onRefresh_withFingerprintUnlockSourceId_setsFingerprintUnlockData() { when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); @@ -332,7 +351,7 @@ public class SafetySourceBroadcastReceiverTest { new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(mSafetyCenterManagerWrapper, times(5)) + verify(mSafetyCenterManagerWrapper, times(6)) .setSafetySourceData(any(), captor.capture(), any(), any()); List safetySourceIdList = captor.getAllValues(); @@ -353,6 +372,11 @@ public class SafetySourceBroadcastReceiverTest { .anyMatch( id -> id.equals(FingerprintSafetySource.SAFETY_SOURCE_ID))) .isTrue(); + assertThat( + safetySourceIdList.stream() + .anyMatch( + id -> id.equals(WearSafetySource.SAFETY_SOURCE_ID))) + .isTrue(); assertThat( safetySourceIdList.stream() .anyMatch( diff --git a/tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java new file mode 100644 index 00000000000..c0c982d44fe --- /dev/null +++ b/tests/unit/src/com/android/settings/safetycenter/WearSafetySourceTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2025 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.safetycenter; + +import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserHandle; +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 android.provider.DeviceConfig; +import android.provider.Settings; +import android.safetycenter.SafetyEvent; +import android.safetycenter.SafetySourceData; +import android.safetycenter.SafetySourceStatus; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils; +import com.android.settings.flags.Flags; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.ResourcesUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class WearSafetySourceTest { + + private static final ComponentName COMPONENT_NAME = new ComponentName("package", "class"); + private static final UserHandle USER_HANDLE = new UserHandle(UserHandle.myUserId()); + private static final SafetyEvent EVENT_SOURCE_STATE_CHANGED = + new SafetyEvent.Builder(SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build(); + public static final String TARGET = "com.active.unlock.target"; + public static final String PROVIDER = "com.active.unlock.provider"; + public static final String TARGET_SETTING = "active_unlock_target"; + public static final String PROVIDER_SETTING = "active_unlock_provider"; + public static final String SUMMARY = "Wear Summary"; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private Context mApplicationContext; + + @Mock private PackageManager mPackageManager; + @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock private FingerprintManager mFingerprintManager; + @Mock private LockPatternUtils mLockPatternUtils; + @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mApplicationContext = spy(ApplicationProvider.getApplicationContext()); + when(mApplicationContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(mDevicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(USER_HANDLE)) + .thenReturn(COMPONENT_NAME); + when(mApplicationContext.getSystemService(Context.FINGERPRINT_SERVICE)) + .thenReturn(mFingerprintManager); + when(mApplicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE)) + .thenReturn(mDevicePolicyManager); + FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest(); + when(featureFactory.securityFeatureProvider.getLockPatternUtils(mApplicationContext)) + .thenReturn(mLockPatternUtils); + doReturn(true).when(mLockPatternUtils).isSecure(anyInt()); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + } + + @After + public void tearDown() { + SafetyCenterManagerWrapper.sInstance = null; + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetyData_whenSafetyCenterIsDisabled_doesNotSetData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(false); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_whenSeparateBiometricsFlagOff_setsNullData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), eq(WearSafetySource.SAFETY_SOURCE_ID), eq(null), any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_whenSafetyCenterIsEnabled_activeUnlockDisabled_setsNullData() { + disableActiveUnlock(mApplicationContext); + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), eq(WearSafetySource.SAFETY_SOURCE_ID), eq(null), any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_setsDataWithCorrectSafetyEvent() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData(any(), any(), any(), eq(EVENT_SOURCE_STATE_CHANGED)); + } + + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_withWearEnabled_whenWearEnrolled_setsData() { + enableActiveUnlock(mApplicationContext); + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(false); + + WearSafetySource.setHasEnrolledForTesting(true); + WearSafetySource.setSummaryForTesting(SUMMARY); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + assertSafetySourceEnabledDataSet( + ResourcesUtils.getResourcesString(mApplicationContext, + "security_settings_activeunlock"), + SUMMARY); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void setSafetySourceData_withWearEnabled_whenWearNotEnrolled_setsData() { + enableActiveUnlock(mApplicationContext); + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(false); + when(mDevicePolicyManager.getKeyguardDisabledFeatures(COMPONENT_NAME)).thenReturn(0); + + WearSafetySource.setHasEnrolledForTesting(false); + WearSafetySource.setSummaryForTesting(SUMMARY); + + WearSafetySource.setSafetySourceData( + mApplicationContext, EVENT_SOURCE_STATE_CHANGED); + + assertSafetySourceDisabledDataSet( + ResourcesUtils.getResourcesString(mApplicationContext, + "security_settings_activeunlock"), + SUMMARY); + } + + private static void disableActiveUnlock(Context context) { + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_REMOTE_AUTH, + ActiveUnlockStatusUtils.CONFIG_FLAG_NAME, + /* value= */ null, + /* makeDefault=*/ false); + Settings.Secure.putString(context.getContentResolver(), TARGET_SETTING, null); + Settings.Secure.putString(context.getContentResolver(), PROVIDER_SETTING, null); + } + + private static void enableActiveUnlock(Context context) { + Settings.Secure.putString( + context.getContentResolver(), TARGET_SETTING, TARGET); + Settings.Secure.putString( + context.getContentResolver(), PROVIDER_SETTING, PROVIDER); + + PackageManager packageManager = context.getPackageManager(); + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; + + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = applicationInfo; + when(packageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo); + + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.authority = PROVIDER; + providerInfo.applicationInfo = applicationInfo; + when(packageManager.resolveContentProvider(anyString(), any())).thenReturn(providerInfo); + + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_REMOTE_AUTH, + ActiveUnlockStatusUtils.CONFIG_FLAG_NAME, + "unlock_intent_layout", + false /* makeDefault */); + } + + private void assertSafetySourceDisabledDataSet(String expectedTitle, String expectedSummary) { + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), + eq(WearSafetySource.SAFETY_SOURCE_ID), + captor.capture(), + any()); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + + assertThat(safetySourceStatus.getTitle().toString()).isEqualTo(expectedTitle); + assertThat(safetySourceStatus.getSummary().toString()).isEqualTo(expectedSummary); + assertThat(safetySourceStatus.isEnabled()).isTrue(); + assertThat(safetySourceStatus.getSeverityLevel()) + .isEqualTo(SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED); + + Intent clickIntent = safetySourceStatus.getPendingIntent().getIntent(); + assertThat(clickIntent).isNotNull(); + assertThat(clickIntent.getAction()).isEqualTo(TARGET); + } + + private void assertSafetySourceEnabledDataSet( + String expectedTitle, String expectedSummary) { + ArgumentCaptor captor = ArgumentCaptor.forClass(SafetySourceData.class); + verify(mSafetyCenterManagerWrapper) + .setSafetySourceData( + any(), + eq(WearSafetySource.SAFETY_SOURCE_ID), + captor.capture(), + any()); + SafetySourceData safetySourceData = captor.getValue(); + SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); + + assertThat(safetySourceStatus.getTitle().toString()).isEqualTo(expectedTitle); + assertThat(safetySourceStatus.getSummary().toString()).isEqualTo(expectedSummary); + assertThat(safetySourceStatus.isEnabled()).isTrue(); + assertThat(safetySourceStatus.getSeverityLevel()) + .isEqualTo(SafetySourceData.SEVERITY_LEVEL_INFORMATION); + Intent clickIntent = safetySourceStatus.getPendingIntent().getIntent(); + assertThat(clickIntent).isNotNull(); + } +}