From d3335c27857e10bff490cf146f252974240cdfbc Mon Sep 17 00:00:00 2001 From: Tetiana Meronyk Date: Thu, 13 Apr 2023 11:27:22 +0000 Subject: [PATCH 01/21] Make user creation flow in a single dialog Bug: 266548780 Test: atest UserDetailsSettingsTest Change-Id: I3df846aa480b80321269ac7b9b8723912597aba4 --- .../android/settings/users/UserSettings.java | 98 ++++++------------- 1 file changed, 32 insertions(+), 66 deletions(-) diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index d904ed0f55c..28e02ec16e2 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -28,7 +28,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; @@ -83,6 +82,7 @@ import com.android.settingslib.RestrictedPreference; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.search.SearchIndexableRaw; +import com.android.settingslib.users.CreateUserDialogController; import com.android.settingslib.users.EditUserInfoController; import com.android.settingslib.users.GrantAdminDialogController; import com.android.settingslib.users.UserCreatingDialog; @@ -119,6 +119,7 @@ public class UserSettings extends SettingsPreferenceFragment /** UserId of the user being removed */ private static final String SAVE_REMOVING_USER = "removing_user"; + private static final String SAVE_CREATE_USER = "create_user"; private static final String KEY_USER_LIST = "user_list"; private static final String KEY_USER_ME = "user_me"; @@ -171,9 +172,6 @@ public class UserSettings extends SettingsPreferenceFragment static final int RESULT_GUEST_REMOVED = 100; - private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED = - "key_add_user_long_message_displayed"; - private static final String KEY_TITLE = "title"; private static final String KEY_SUMMARY = "summary"; @@ -222,6 +220,8 @@ public class UserSettings extends SettingsPreferenceFragment new GrantAdminDialogController(); private EditUserInfoController mEditUserInfoController = new EditUserInfoController(Utils.FILE_PROVIDER_AUTHORITY); + private CreateUserDialogController mCreateUserDialogController = + new CreateUserDialogController(Utils.FILE_PROVIDER_AUTHORITY); private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController; private GuestTelephonyPreferenceController mGuestTelephonyPreferenceController; private RemoveGuestOnExitPreferenceController mRemoveGuestOnExitPreferenceController; @@ -233,7 +233,7 @@ public class UserSettings extends SettingsPreferenceFragment private CharSequence mPendingUserName; private Drawable mPendingUserIcon; - private boolean mGrantAdmin; + private boolean mPendingUserIsAdmin; // A place to cache the generated default avatar private Drawable mDefaultIconDrawable; @@ -348,7 +348,11 @@ public class UserSettings extends SettingsPreferenceFragment if (icicle.containsKey(SAVE_REMOVING_USER)) { mRemovingUserId = icicle.getInt(SAVE_REMOVING_USER); } - mEditUserInfoController.onRestoreInstanceState(icicle); + if (icicle.containsKey(SAVE_CREATE_USER)) { + mCreateUserDialogController.onRestoreInstanceState(icicle); + } else { + mEditUserInfoController.onRestoreInstanceState(icicle); + } } mUserCaps = UserCapabilities.create(activity); @@ -440,7 +444,12 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void onSaveInstanceState(Bundle outState) { - mEditUserInfoController.onSaveInstanceState(outState); + if (mCreateUserDialogController.isActive()) { + outState.putBoolean(SAVE_CREATE_USER, mCreateUserDialogController.isActive()); + mCreateUserDialogController.onSaveInstanceState(outState); + } else { + mEditUserInfoController.onSaveInstanceState(outState); + } outState.putInt(SAVE_REMOVING_USER, mRemovingUserId); super.onSaveInstanceState(outState); } @@ -448,6 +457,7 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void startActivityForResult(Intent intent, int requestCode) { mEditUserInfoController.startingActivityForResult(); + mCreateUserDialogController.startingActivityForResult(); super.startActivityForResult(intent, requestCode); } @@ -562,6 +572,7 @@ public class UserSettings extends SettingsPreferenceFragment && resultCode == RESULT_GUEST_REMOVED) { scheduleGuestCreation(); } else { + mCreateUserDialogController.onActivityResult(requestCode, resultCode, data); mEditUserInfoController.onActivityResult(requestCode, resultCode, data); } } @@ -704,37 +715,12 @@ public class UserSettings extends SettingsPreferenceFragment .setPositiveButton(android.R.string.ok, null) .create(); case DIALOG_ADD_USER: { - final SharedPreferences preferences = getActivity().getPreferences( - Context.MODE_PRIVATE); - final boolean longMessageDisplayed = preferences.getBoolean( - KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, false); - final int messageResId = longMessageDisplayed - ? com.android.settingslib.R.string.user_add_user_message_short - : com.android.settingslib.R.string.user_add_user_message_long; - Dialog dlg = new AlertDialog.Builder(context) - .setTitle(com.android.settingslib.R.string.user_add_user_title) - .setMessage(messageResId) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (!longMessageDisplayed) { - preferences.edit().putBoolean( - KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, - true).apply(); - } - if (UserManager.isMultipleAdminEnabled()) { - showDialog(DIALOG_GRANT_ADMIN); - } else { - showDialog(DIALOG_USER_PROFILE_EDITOR_ADD_USER); - } - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - return dlg; - } - case DIALOG_GRANT_ADMIN: { - return buildGrantAdminDialog(); + synchronized (mUserLock) { + mPendingUserName = getString( + com.android.settingslib.R.string.user_new_user_name); + mPendingUserIcon = null; + } + return buildAddUserDialog(USER_TYPE_USER); } case DIALOG_CHOOSE_USER_TYPE: { List> data = new ArrayList>(); @@ -919,17 +905,14 @@ public class UserSettings extends SettingsPreferenceFragment private Dialog buildAddUserDialog(int userType) { Dialog d; synchronized (mUserLock) { - d = mEditUserInfoController.createDialog( + d = mCreateUserDialogController.createDialog( getActivity(), this::startActivityForResult, - null, - mPendingUserName.toString(), - getString(userType == USER_TYPE_USER - ? com.android.settingslib.R.string.user_info_settings_title - : com.android.settingslib.R.string.profile_info_settings_title), - (userName, userIcon) -> { + UserManager.isMultipleAdminEnabled(), + (userName, userIcon, isAdmin) -> { mPendingUserIcon = userIcon; mPendingUserName = userName; + mPendingUserIsAdmin = isAdmin; addUserNow(userType); }, () -> { @@ -943,26 +926,6 @@ public class UserSettings extends SettingsPreferenceFragment return d; } - private Dialog buildGrantAdminDialog() { - return mGrantAdminDialogController.createDialog( - getActivity(), - (grantAdmin) -> { - mGrantAdmin = grantAdmin; - if (mGrantAdmin) { - mMetricsFeatureProvider.action(getActivity(), - SettingsEnums.ACTION_GRANT_ADMIN_FROM_SETTINGS_CREATION_DIALOG); - } else { - mMetricsFeatureProvider.action(getActivity(), - SettingsEnums.ACTION_NOT_GRANT_ADMIN_FROM_SETTINGS_CREATION_DIALOG); - } - showDialog(DIALOG_USER_PROFILE_EDITOR_ADD_USER); - }, - () -> { - mGrantAdmin = false; - } - ); - } - @Override public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { @@ -1065,7 +1028,7 @@ public class UserSettings extends SettingsPreferenceFragment userName, mUserManager.USER_TYPE_FULL_SECONDARY, 0); - if (mGrantAdmin) { + if (mPendingUserIsAdmin) { mUserManager.setUserAdmin(user.id); } } else { @@ -1665,6 +1628,9 @@ public class UserSettings extends SettingsPreferenceFragment synchronized (mUserLock) { mRemovingUserId = -1; updateUserList(); + if (mCreateUserDialogController.isActive()) { + mCreateUserDialogController.clear(); + } } } From eac02feb2d3868c3db2e37e39db8edaac410c0b2 Mon Sep 17 00:00:00 2001 From: Bill Yi Date: Tue, 9 May 2023 19:02:21 -0700 Subject: [PATCH 02/21] Import translations. DO NOT MERGE ANYWHERE Auto-generated-cl: translation import Change-Id: I752b8c12cf13fcb989dbb335b3230198d3be3201 --- res/values-pl/arrays.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/values-pl/arrays.xml b/res/values-pl/arrays.xml index 68f5d71dac8..55408c4302a 100644 --- a/res/values-pl/arrays.xml +++ b/res/values-pl/arrays.xml @@ -30,13 +30,13 @@ "Wszystkie" - "15 sekund" - "30 sekund" - "1 minuta" - "2 minuty" - "5 minut" - "10 minut" - "30 minut" + "15 sek." + "30 sek." + "1 min" + "2 min" + "5 min" + "10 min" + "30 min" "Nigdy" From 755a67a9313b01a9c25c0aed6a14a195a0bde980 Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Fri, 5 May 2023 23:15:11 +0000 Subject: [PATCH 03/21] Set EXTRA_KEY_FOR_FINGERPRINT for choose lock screen. Bug: 279823572 Test: atest FingerprintSettingsFragmentTest Test: atest ChooseLockGenericTest Change-Id: Ie4bc18bf245eb7a755862401c710d85381063ff9 Merged-In: Ie4bc18bf245eb7a755862401c710d85381063ff9 --- res/values/strings.xml | 12 +-- .../fingerprint/FingerprintSettings.java | 5 +- .../FingerprintSettingsFragmentTest.java | 100 +++++++++++------- .../password/ChooseLockGenericTest.java | 74 +++++++++---- 4 files changed, 122 insertions(+), 69 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 8fc5ed5d651..aa0671eb438 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1182,20 +1182,20 @@ Current screen lock - Fingerprint + Pattern + Pattern \u2022 Fingerprint - Fingerprint + PIN + PIN \u2022 Fingerprint - Fingerprint + Password + Password \u2022 Fingerprint Continue without fingerprint - Face Unlock + Pattern + Pattern \u2022 Face - Face Unlock + PIN + PIN \u2022 Face - Face Unlock + Password + Password \u2022 Face Continue without Face Unlock diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index e0f402bdc02..be090e33366 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -185,7 +185,8 @@ public class FingerprintSettings extends SubSettings { private static final int MSG_FINGER_AUTH_HELP = 1004; private static final int CONFIRM_REQUEST = 101; - private static final int CHOOSE_LOCK_GENERIC_REQUEST = 102; + @VisibleForTesting + static final int CHOOSE_LOCK_GENERIC_REQUEST = 102; @VisibleForTesting static final int ADD_FINGERPRINT_REQUEST = 10; private static final int AUTO_ADD_FIRST_FINGERPRINT_REQUEST = 11; @@ -1014,7 +1015,7 @@ public class FingerprintSettings extends SubSettings { true); intent.putExtra(Intent.EXTRA_USER_ID, mUserId); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true); - intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true); startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST); } } diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java index 2bc81e60a06..18b05add7ad 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java @@ -20,8 +20,11 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFP import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment; import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.ADD_FINGERPRINT_REQUEST; +import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.CHOOSE_LOCK_GENERIC_REQUEST; import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.KEY_FINGERPRINT_ADD; +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.anyString; @@ -54,6 +57,7 @@ import com.android.settings.biometrics.BiometricsSplitScreenDialog; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowFragment; +import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import com.android.settings.testutils.shadow.ShadowSettingsPreferenceFragment; import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settings.testutils.shadow.ShadowUtils; @@ -63,6 +67,7 @@ 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.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -74,7 +79,7 @@ import java.util.ArrayList; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowSettingsPreferenceFragment.class, ShadowUtils.class, ShadowFragment.class, - ShadowUserManager.class}) + ShadowUserManager.class, ShadowLockPatternUtils.class}) public class FingerprintSettingsFragmentTest { private FingerprintSettingsFragment mFragment; private Context mContext; @@ -92,10 +97,62 @@ public class FingerprintSettingsFragmentTest { doReturn(true).when(mFingerprintManager).isHardwareDetected(); ShadowUtils.setFingerprintManager(mFingerprintManager); FakeFeatureFactory.setupForTest(); + } + @After + public void tearDown() { + ShadowUtils.reset(); + } + + @Test + public void testAddFingerprint_inFullScreen_noDialog() { + setUpFragment(false); + // Click "Add Fingerprint" + final Preference preference = new Preference(mContext); + preference.setKey(KEY_FINGERPRINT_ADD); + mFragment.onPreferenceTreeClick(preference); + + verify(mFragment).startActivityForResult(any(), eq(ADD_FINGERPRINT_REQUEST)); + verify(mFragmentTransaction, never()).add(any(), + eq(BiometricsSplitScreenDialog.class.getName())); + + } + + @Test + public void testAddFingerprint_inMultiWindow_showsDialog() { + setUpFragment(false); + + doReturn(true).when(mActivity).isInMultiWindowMode(); + + // Click "Add Fingerprint" + final Preference preference = new Preference(mContext); + preference.setKey(KEY_FINGERPRINT_ADD); + mFragment.onPreferenceTreeClick(preference); + + verify(mFragment, times(0)).startActivityForResult(any(), eq(ADD_FINGERPRINT_REQUEST)); + verify(mFragmentTransaction).add(any(), eq(BiometricsSplitScreenDialog.class.getName())); + } + + @Test + public void testChooseLockKeyForFingerprint() { + setUpFragment(true); + ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass( + Intent.class); + verify(mFragment).startActivityForResult(intentArgumentCaptor.capture(), + eq(CHOOSE_LOCK_GENERIC_REQUEST)); + + Intent intent = intentArgumentCaptor.getValue(); + assertThat(intent.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, + false)).isTrue(); + } + + private void setUpFragment(boolean showChooseLock) { Intent intent = new Intent(); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L); + if (!showChooseLock) { + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L); + } + mActivity = spy(Robolectric.buildActivity(FragmentActivity.class, intent).get()); mContext = spy(ApplicationProvider.getApplicationContext()); @@ -112,49 +169,12 @@ public class FingerprintSettingsFragmentTest { doNothing().when(mFragment).startActivityForResult(any(Intent.class), anyInt()); setSensor(); - } - @After - public void tearDown() { - ShadowUtils.reset(); - } - - @Test - public void testAddFingerprint_inFullScreen_noDialog() { // Start fragment mFragment.onAttach(mContext); mFragment.onCreate(null); mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY); mFragment.onResume(); - - // Click "Add Fingerprint" - final Preference preference = new Preference(mContext); - preference.setKey(KEY_FINGERPRINT_ADD); - mFragment.onPreferenceTreeClick(preference); - - verify(mFragment).startActivityForResult(any(), eq(ADD_FINGERPRINT_REQUEST)); - verify(mFragmentTransaction, never()).add(any(), - eq(BiometricsSplitScreenDialog.class.getName())); - - } - - @Test - public void testAddFingerprint_inMultiWindow_showsDialog() { - // Start fragment - mFragment.onAttach(mContext); - mFragment.onCreate(null); - mFragment.onCreateView(LayoutInflater.from(mContext), mock(ViewGroup.class), Bundle.EMPTY); - mFragment.onResume(); - - doReturn(true).when(mActivity).isInMultiWindowMode(); - - // Click "Add Fingerprint" - final Preference preference = new Preference(mContext); - preference.setKey(KEY_FINGERPRINT_ADD); - mFragment.onPreferenceTreeClick(preference); - - verify(mFragment, times(0)).startActivityForResult(any(), eq(ADD_FINGERPRINT_REQUEST)); - verify(mFragmentTransaction).add(any(), eq(BiometricsSplitScreenDialog.class.getName())); } private void setSensor() { diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java index dbc60dce731..12a540d7c33 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockGenericTest.java @@ -30,6 +30,8 @@ import static com.android.settings.password.ChooseLockGeneric.ChooseLockGenericF import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_DEVICE_PASSWORD_REQUIREMENT_ONLY; 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; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_IS_CALLING_APP_ADMIN; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; @@ -126,7 +128,9 @@ public class ChooseLockGenericTest { when(mFaceManager.isHardwareDetected()).thenReturn(true); when(mFingerprintManager.isHardwareDetected()).thenReturn(true); when(mFakeFeatureFactory.mFaceFeatureProvider.isSetupWizardSupported(any())).thenReturn( - false); + true); + ShadowUtils.setFingerprintManager(mFingerprintManager); + ShadowUtils.setFaceManager(mFaceManager); } @After @@ -540,35 +544,63 @@ public class ChooseLockGenericTest { @Test public void updatePreferenceText_supportBiometrics_showFaceAndFingerprint() { - ShadowLockPatternUtils.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW); - final PasswordPolicy policy = new PasswordPolicy(); - policy.quality = PASSWORD_QUALITY_ALPHABETIC; - ShadowLockPatternUtils.setRequestedProfilePasswordMetrics(policy.getMinMetrics()); - + ShadowStorageManager.setIsFileEncrypted(false); final Intent intent = new Intent().putExtra(EXTRA_KEY_FOR_BIOMETRICS, true); initActivity(intent); - final Intent passwordIntent = mFragment.getLockPatternIntent(); - assertThat(passwordIntent.getIntExtra(ChooseLockPassword.EXTRA_KEY_MIN_COMPLEXITY, - PASSWORD_COMPLEXITY_NONE)).isEqualTo(PASSWORD_COMPLEXITY_LOW); final String supportFingerprint = capitalize(mActivity.getResources().getString( R.string.security_settings_fingerprint)); final String supportFace = capitalize(mActivity.getResources().getString( R.string.keywords_face_settings)); + String pinTitle = + (String) mFragment.findPreference(ScreenLockType.PIN.preferenceKey).getTitle(); + String patternTitle = + (String) mFragment.findPreference(ScreenLockType.PATTERN.preferenceKey).getTitle(); + String passwordTitle = + (String) mFragment.findPreference(ScreenLockType.PASSWORD.preferenceKey).getTitle(); - assertThat(mFragment.getBiometricsPreferenceTitle(ScreenLockType.PIN)).contains( - supportFingerprint); - assertThat(mFragment.getBiometricsPreferenceTitle(ScreenLockType.PIN)).contains( - supportFace); - assertThat(mFragment.getBiometricsPreferenceTitle(ScreenLockType.PATTERN)).contains( - supportFingerprint); - assertThat(mFragment.getBiometricsPreferenceTitle(ScreenLockType.PATTERN)).contains( - supportFace); - assertThat(mFragment.getBiometricsPreferenceTitle(ScreenLockType.PASSWORD)).contains( - supportFingerprint); - assertThat(mFragment.getBiometricsPreferenceTitle(ScreenLockType.PASSWORD)).contains( - supportFace); + assertThat(pinTitle).contains(supportFingerprint); + assertThat(pinTitle).contains(supportFace); + assertThat(patternTitle).contains(supportFingerprint); + assertThat(patternTitle).contains(supportFace); + assertThat(passwordTitle).contains(supportFingerprint); + assertThat(passwordTitle).contains(supportFace); + } + + @Test + public void updatePreferenceText_supportFingerprint_showFingerprint() { + ShadowStorageManager.setIsFileEncrypted(false); + final Intent intent = new Intent().putExtra(EXTRA_KEY_FOR_FINGERPRINT, true); + initActivity(intent); + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + + assertThat(mFragment.findPreference(ScreenLockType.PIN.preferenceKey).getTitle()).isEqualTo( + mFragment.getString(R.string.fingerprint_unlock_set_unlock_pin)); + assertThat(mFragment.findPreference( + ScreenLockType.PATTERN.preferenceKey).getTitle()).isEqualTo( + mFragment.getString(R.string.fingerprint_unlock_set_unlock_pattern)); + assertThat(mFragment.findPreference( + ScreenLockType.PASSWORD.preferenceKey).getTitle()).isEqualTo( + mFragment.getString(R.string.fingerprint_unlock_set_unlock_password)); + } + + @Test + public void updatePreferenceText_supportFace_showFace() { + + ShadowStorageManager.setIsFileEncrypted(false); + final Intent intent = new Intent().putExtra(EXTRA_KEY_FOR_FACE, true); + initActivity(intent); + mFragment.updatePreferencesOrFinish(false /* isRecreatingActivity */); + + assertThat(mFragment.findPreference(ScreenLockType.PIN.preferenceKey).getTitle()).isEqualTo( + mFragment.getString(R.string.face_unlock_set_unlock_pin)); + assertThat(mFragment.findPreference( + ScreenLockType.PATTERN.preferenceKey).getTitle()).isEqualTo( + mFragment.getString(R.string.face_unlock_set_unlock_pattern)); + assertThat(mFragment.findPreference( + ScreenLockType.PASSWORD.preferenceKey).getTitle()).isEqualTo( + mFragment.getString(R.string.face_unlock_set_unlock_password)); } private void initActivity(@Nullable Intent intent) { From bc297b9f3cf4e5f7f81437b714fc55502b3d27bf Mon Sep 17 00:00:00 2001 From: Yanting Yang Date: Thu, 11 May 2023 16:29:07 +0800 Subject: [PATCH 04/21] =?UTF-8?q?Make=20=E2=80=9CReset=20app=20preferences?= =?UTF-8?q?=E2=80=9D=20restricted=20by=20DISALLOW=5FAPPS=5FCONTROL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When DISALLOW_APPS_CONTROL restriction is enabled, users should not be able to enable/disable apps, clear app caches and clear app data. “System >Reset options > Reset app preferences” can take the above actions which should be restricted by DISALLOW_APPS_CONTROL to fix the security vulnerability. Bug: 278559731 Test: Verify change by turning on/off DISALLOW_APPS_CONTROL with TestDPC. Change-Id: I387a95aa7f54fe04d5ed92372e43a88d5fe3e79a --- res/xml/reset_dashboard_fragment.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/xml/reset_dashboard_fragment.xml b/res/xml/reset_dashboard_fragment.xml index 98f6c02a8f2..3bd7a136760 100644 --- a/res/xml/reset_dashboard_fragment.xml +++ b/res/xml/reset_dashboard_fragment.xml @@ -38,9 +38,10 @@ settings:controller="com.android.settings.network.BluetoothWiFiResetPreferenceController" /> - + android:title="@string/reset_app_preferences" + settings:userRestriction="no_control_apps" /> Date: Thu, 11 May 2023 17:24:32 +0800 Subject: [PATCH 05/21] Update string for touch sounds Bug: 274081150 Test: visual verify Change-Id: I953f108884cafc91e2a3a76bd1292ea30386d862 --- res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 261829b56ef..b342e8af3a4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7162,7 +7162,7 @@ Docking sounds - Touch sounds + Tap & click sounds Always show icon when in vibrate mode From 08b4973f8edcefab421b0e119ff69f79c7a1421c Mon Sep 17 00:00:00 2001 From: Oli Thompson Date: Fri, 24 Mar 2023 13:01:37 +0000 Subject: [PATCH 06/21] Update Work profile settings Change work apps toggle to a primary toggle make Xprofile contact search toggle disable/enable when work profile is turned off Add footer and change strings Add tests Test: atest ContactSearchPreferenceControllerTest, atest WorkModePreferenceControllerTest Bug: 253009702 275538029 Change-Id: I3b2044a5fe3f2aff0748d66e701a3f0d7667ab7a --- res/values/strings.xml | 10 +- res/xml/managed_profile_settings.xml | 8 +- .../ContactSearchPreferenceController.java | 79 ++++++--- .../ManagedProfileQuietModeEnabler.java | 122 +++++++++++++ .../WorkModePreferenceController.java | 161 +++++------------- ...ContactSearchPreferenceControllerTest.java | 110 ++++++++++-- .../ManagedProfileQuietModeEnablerTest.java | 126 ++++++++++++++ .../WorkModePreferenceControllerTest.java | 66 ++++--- 8 files changed, 482 insertions(+), 200 deletions(-) create mode 100644 src/com/android/settings/accounts/ManagedProfileQuietModeEnabler.java create mode 100644 tests/robotests/src/com/android/settings/accounts/ManagedProfileQuietModeEnablerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 793a9397e36..2f0b10f0b79 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5873,12 +5873,8 @@ Add account Work profile isn\u2019t available yet - - Work profile - - Managed by your organization - - Apps and notifications are off + + Work apps Remove work profile @@ -9779,6 +9775,8 @@ Cross-profile calendar Show work events on your personal calendar + + When work apps are off, they’re paused and can’t be accessed or send you notifications Manage storage diff --git a/res/xml/managed_profile_settings.xml b/res/xml/managed_profile_settings.xml index 20f6d3dc3f3..ddfb86987ca 100644 --- a/res/xml/managed_profile_settings.xml +++ b/res/xml/managed_profile_settings.xml @@ -19,10 +19,9 @@ android:key="managed_profile_settings_screen" android:title="@string/managed_profile_settings_title"> - + + \ No newline at end of file diff --git a/src/com/android/settings/accounts/ContactSearchPreferenceController.java b/src/com/android/settings/accounts/ContactSearchPreferenceController.java index 5e4ef48d7c2..87dabd8d9a4 100644 --- a/src/com/android/settings/accounts/ContactSearchPreferenceController.java +++ b/src/com/android/settings/accounts/ContactSearchPreferenceController.java @@ -20,37 +20,38 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.TogglePreferenceController; import com.android.settings.slices.SliceData; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; -public class ContactSearchPreferenceController extends BasePreferenceController implements - Preference.OnPreferenceChangeListener { +import org.jetbrains.annotations.NotNull; - private UserHandle mManagedUser; +public class ContactSearchPreferenceController extends TogglePreferenceController implements + Preference.OnPreferenceChangeListener, DefaultLifecycleObserver, + ManagedProfileQuietModeEnabler.QuietModeChangeListener { + + private final ManagedProfileQuietModeEnabler mQuietModeEnabler; + private final UserHandle mManagedUser; + private Preference mPreference; public ContactSearchPreferenceController(Context context, String key) { super(context, key); - // Set default managed profile for the current user, otherwise isAvailable will be false and - // the setting won't be searchable. - UserManager userManager = context.getSystemService(UserManager.class); - mManagedUser = Utils.getManagedProfile(userManager); - } - - @VisibleForTesting - void setManagedUser(UserHandle managedUser) { - mManagedUser = managedUser; + mManagedUser = Utils.getManagedProfile(context.getSystemService(UserManager.class)); + mQuietModeEnabler = new ManagedProfileQuietModeEnabler(context, this); } @Override public int getAvailabilityStatus() { - return (mManagedUser != null) ? AVAILABLE : DISABLED_FOR_USER; + return mQuietModeEnabler.isAvailable() ? AVAILABLE : DISABLED_FOR_USER; } @Override @@ -59,6 +60,7 @@ public class ContactSearchPreferenceController extends BasePreferenceController if (preference instanceof RestrictedSwitchPreference) { final RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; pref.setChecked(isChecked()); + pref.setEnabled(!mQuietModeEnabler.isQuietModeEnabled()); if (mManagedUser != null) { final RestrictedLockUtils.EnforcedAdmin enforcedAdmin = RestrictedLockUtilsInternal.checkIfRemoteContactSearchDisallowed( @@ -68,26 +70,48 @@ public class ContactSearchPreferenceController extends BasePreferenceController } } - private boolean isChecked() { - if (mManagedUser == null) { + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + updateState(mPreference); + } + + @Override + public void onStart(@NotNull LifecycleOwner lifecycleOwner) { + lifecycleOwner.getLifecycle().addObserver(mQuietModeEnabler); + } + + @Override + public void onStop(@NotNull LifecycleOwner lifecycleOwner) { + lifecycleOwner.getLifecycle().removeObserver(mQuietModeEnabler); + } + + @Override + public boolean isChecked() { + if (mManagedUser == null || mQuietModeEnabler.isQuietModeEnabled()) { return false; } return 0 != Settings.Secure.getIntForUser(mContext.getContentResolver(), MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, mManagedUser.getIdentifier()); } - private boolean setChecked(boolean isChecked) { - if (mManagedUser != null) { - final int value = isChecked ? 1 : 0; - Settings.Secure.putIntForUser(mContext.getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, value, mManagedUser.getIdentifier()); + @Override + public boolean setChecked(boolean isChecked) { + if (mManagedUser == null || mQuietModeEnabler.isQuietModeEnabled()) { + return false; } + final int value = isChecked ? 1 : 0; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, value, mManagedUser.getIdentifier()); return true; } @Override - public final boolean onPreferenceChange(Preference preference, Object newValue) { - return setChecked((boolean) newValue); + public void onQuietModeChanged() { + if (mPreference != null) { + updateState(mPreference); + } } @Override @@ -95,4 +119,9 @@ public class ContactSearchPreferenceController extends BasePreferenceController public int getSliceType() { return SliceData.SliceType.SWITCH; } -} \ No newline at end of file + + @Override + public int getSliceHighlightMenuRes() { + return R.string.menu_key_accounts; + } +} diff --git a/src/com/android/settings/accounts/ManagedProfileQuietModeEnabler.java b/src/com/android/settings/accounts/ManagedProfileQuietModeEnabler.java new file mode 100644 index 00000000000..989be09565c --- /dev/null +++ b/src/com/android/settings/accounts/ManagedProfileQuietModeEnabler.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accounts; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + +import com.android.settings.Utils; + +import javax.annotation.Nullable; + +/** + * A class that controls the managed profile's quiet mode, listens to quiet mode changes and + * modifies managed profile settings. The user facing term for quiet mode is "work apps". + */ +final class ManagedProfileQuietModeEnabler implements DefaultLifecycleObserver { + + private static final String TAG = "QuietModeEnabler"; + private final Context mContext; + private final QuietModeChangeListener mListener; + @Nullable private final UserHandle mManagedProfile; + private final UserManager mUserManager; + + public interface QuietModeChangeListener { + /** Called when quiet mode has changed. */ + void onQuietModeChanged(); + } + + ManagedProfileQuietModeEnabler(Context context, QuietModeChangeListener listener) { + mContext = context; + mListener = listener; + mUserManager = context.getSystemService(UserManager.class); + mManagedProfile = Utils.getManagedProfile(mUserManager); + } + + public void setQuietModeEnabled(boolean enabled) { + if (mManagedProfile != null) { + mUserManager.requestQuietModeEnabled(enabled, mManagedProfile); + } + } + + public boolean isQuietModeEnabled() { + return mManagedProfile != null && mUserManager.isQuietModeEnabled(mManagedProfile); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + mContext.unregisterReceiver(mReceiver); + } + + public boolean isAvailable() { + return (mManagedProfile != null); + } + + private void refreshQuietMode() { + if (mListener != null) { + mListener.onQuietModeChanged(); + } + } + + /** + * Receiver that listens to {@link Intent#ACTION_MANAGED_PROFILE_AVAILABLE} and + * {@link Intent#ACTION_MANAGED_PROFILE_UNAVAILABLE}, and updates the work mode + */ + @VisibleForTesting + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + String action = intent.getAction(); + Log.v(TAG, "Received broadcast: " + action); + + if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) + || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { + int intentUserIdentifier = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL); + if (intentUserIdentifier == mManagedProfile.getIdentifier()) { + refreshQuietMode(); + } else { + Log.w(TAG, "Managed profile broadcast ID: " + intentUserIdentifier + + " does not match managed user: " + mManagedProfile); + } + } else { + Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); + } + } + }; +} diff --git a/src/com/android/settings/accounts/WorkModePreferenceController.java b/src/com/android/settings/accounts/WorkModePreferenceController.java index 1941de45c4d..a261afccaa6 100644 --- a/src/com/android/settings/accounts/WorkModePreferenceController.java +++ b/src/com/android/settings/accounts/WorkModePreferenceController.java @@ -1,165 +1,90 @@ /* * Copyright (C) 2018 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 + * 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. + * 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.accounts; -import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_SETTING_OFF_SUMMARY; -import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_SETTING_ON_SUMMARY; - -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; -import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settings.core.BasePreferenceController; import com.android.settings.slices.SliceData; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnStart; -import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settings.widget.SettingsMainSwitchPreferenceController; +import com.android.settingslib.widget.MainSwitchPreference; -public class WorkModePreferenceController extends BasePreferenceController implements - Preference.OnPreferenceChangeListener, LifecycleObserver, OnStart, OnStop { +import org.jetbrains.annotations.NotNull; - private static final String TAG = "WorkModeController"; - private UserManager mUserManager; - private UserHandle mManagedUser; - private DevicePolicyManager mDevicePolicyManager; +public class WorkModePreferenceController extends SettingsMainSwitchPreferenceController + implements Preference.OnPreferenceChangeListener, DefaultLifecycleObserver, + ManagedProfileQuietModeEnabler.QuietModeChangeListener { - private Preference mPreference; - private IntentFilter mIntentFilter; + private final ManagedProfileQuietModeEnabler mQuietModeEnabler; public WorkModePreferenceController(Context context, String key) { super(context, key); - mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); - mIntentFilter = new IntentFilter(); - mIntentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); - mIntentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); - // Set default managed profile for the current user, otherwise isAvailable will be false and - // the setting won't be searchable. - mManagedUser = Utils.getManagedProfile(mUserManager); - } - - @VisibleForTesting - void setManagedUser(UserHandle managedUser) { - mManagedUser = managedUser; - } - - @Override - public void onStart() { - mContext.registerReceiver(mReceiver, mIntentFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); - } - - @Override - public void onStop() { - mContext.unregisterReceiver(mReceiver); + mQuietModeEnabler = new ManagedProfileQuietModeEnabler(context, this); } @Override public int getAvailabilityStatus() { - return (mManagedUser != null) ? AVAILABLE : DISABLED_FOR_USER; + return (mQuietModeEnabler.isAvailable()) ? AVAILABLE : DISABLED_FOR_USER; } @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(getPreferenceKey()); + public void onStart(@NotNull LifecycleOwner lifecycleOwner) { + lifecycleOwner.getLifecycle().addObserver(mQuietModeEnabler); } @Override - public CharSequence getSummary() { - if (isChecked()) { - return mDevicePolicyManager.getResources().getString( - WORK_PROFILE_SETTING_ON_SUMMARY, - () -> mContext.getString(R.string.work_mode_on_summary)); - } - - return mDevicePolicyManager.getResources().getString( - WORK_PROFILE_SETTING_OFF_SUMMARY, - () -> mContext.getString(R.string.work_mode_off_summary)); + public void onStop(@NotNull LifecycleOwner lifecycleOwner) { + lifecycleOwner.getLifecycle().removeObserver(mQuietModeEnabler); } - private boolean isChecked() { - boolean isWorkModeOn = false; - if (mUserManager != null && mManagedUser != null) { - isWorkModeOn = !mUserManager.isQuietModeEnabled(mManagedUser); - } - return isWorkModeOn; + @Override + public boolean isChecked() { + return !mQuietModeEnabler.isQuietModeEnabled(); } - private boolean setChecked(boolean isChecked) { - if (mUserManager != null && mManagedUser != null) { - final boolean quietModeEnabled = !isChecked; - mUserManager.requestQuietModeEnabled(quietModeEnabled, mManagedUser); - } + @Override + public boolean setChecked(boolean isChecked) { + mQuietModeEnabler.setQuietModeEnabled(!isChecked); return true; } @Override - public void updateState(Preference preference) { - super.updateState(preference); - if (preference instanceof TwoStatePreference) { - ((TwoStatePreference) preference).setChecked(isChecked()); - } + public void onQuietModeChanged() { + updateState(mSwitchPreference); } - @Override - public final boolean onPreferenceChange(Preference preference, Object newValue) { - return setChecked((boolean) newValue); - } - - /** - * Receiver that listens to {@link Intent#ACTION_MANAGED_PROFILE_AVAILABLE} and - * {@link Intent#ACTION_MANAGED_PROFILE_UNAVAILABLE}, and updates the work mode - */ - @VisibleForTesting - final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent == null) { - return; - } - final String action = intent.getAction(); - Log.v(TAG, "Received broadcast: " + action); - - if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) - || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { - if (intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL) == mManagedUser.getIdentifier()) { - updateState(mPreference); - } - return; - } - Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); - } - }; - @Override @SliceData.SliceType public int getSliceType() { return SliceData.SliceType.SWITCH; } -} \ No newline at end of file + + @Override + public int getSliceHighlightMenuRes() { + return R.string.menu_key_accounts; + } + + @VisibleForTesting + void setPreference(MainSwitchPreference preference) { + mSwitchPreference = preference; + } +} diff --git a/tests/robotests/src/com/android/settings/accounts/ContactSearchPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accounts/ContactSearchPreferenceControllerTest.java index c8606e134a5..bc65563650f 100644 --- a/tests/robotests/src/com/android/settings/accounts/ContactSearchPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accounts/ContactSearchPreferenceControllerTest.java @@ -20,13 +20,19 @@ import static android.provider.Settings.Secure.MANAGED_PROFILE_CONTACT_REMOTE_SE import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.UserInfo; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; +import androidx.test.core.app.ApplicationProvider; + import com.android.settingslib.RestrictedSwitchPreference; import org.junit.Before; @@ -35,39 +41,51 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; + +import java.util.Collections; @RunWith(RobolectricTestRunner.class) public class ContactSearchPreferenceControllerTest { private static final String PREF_KEY = "contacts_search"; - - @Mock - private UserHandle mManagedUser; + private static final int MANAGED_USER_ID = 10; private Context mContext; private ContactSearchPreferenceController mController; private RestrictedSwitchPreference mPreference; + @Mock + private UserHandle mManagedUser; + @Mock + private UserManager mUserManager; + @Mock + private UserInfo mUserInfo; + @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mController = new ContactSearchPreferenceController(mContext, PREF_KEY); - mController.setManagedUser(mManagedUser); + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); mPreference = spy(new RestrictedSwitchPreference(mContext)); + when(mUserInfo.isManagedProfile()).thenReturn(true); + when(mUserManager.getUserInfo(anyInt())).thenReturn(mUserInfo); + when(mUserManager.getProcessUserId()).thenReturn(0); + when(mUserManager.getUserProfiles()).thenReturn(Collections.singletonList(mManagedUser)); + when(mManagedUser.getIdentifier()).thenReturn(MANAGED_USER_ID); + mController = new ContactSearchPreferenceController(mContext, PREF_KEY); } @Test public void getAvailabilityStatus_noManagedUser_DISABLED() { - mController.setManagedUser(null); + when(mUserManager.getProcessUserId()).thenReturn(MANAGED_USER_ID); + mController = new ContactSearchPreferenceController(mContext, PREF_KEY); + assertThat(mController.getAvailabilityStatus()) .isNotEqualTo(ContactSearchPreferenceController.AVAILABLE); } @Test public void getAvailabilityStatus_hasManagedUser_AVAILABLE() { - mController.setManagedUser(mManagedUser); assertThat(mController.getAvailabilityStatus()) .isEqualTo(ContactSearchPreferenceController.AVAILABLE); } @@ -75,32 +93,96 @@ public class ContactSearchPreferenceControllerTest { @Test public void updateState_shouldRefreshContent() { Settings.Secure.putIntForUser(mContext.getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, mManagedUser.getIdentifier()); + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, MANAGED_USER_ID); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isFalse(); Settings.Secure.putIntForUser(mContext.getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 1, mManagedUser.getIdentifier()); + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 1, MANAGED_USER_ID); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isTrue(); } @Test public void updateState_preferenceShouldBeDisabled() { mController.updateState(mPreference); + verify(mPreference).setDisabledByAdmin(any()); } @Test public void onPreferenceChange_shouldUpdateProviderValue() { mController.onPreferenceChange(mPreference, false); + assertThat(Settings.Secure.getIntForUser(mContext.getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 1, mManagedUser.getIdentifier())) + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 1, MANAGED_USER_ID)) .isEqualTo(0); mController.onPreferenceChange(mPreference, true); + assertThat(Settings.Secure.getIntForUser(mContext.getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, mManagedUser.getIdentifier())) + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, MANAGED_USER_ID)) .isEqualTo(1); } -} \ No newline at end of file + + @Test + public void onQuietModeDisabled_preferenceEnabled() { + when(mUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isTrue(); + } + + @Test + public void onQuietModeEnabled_preferenceDisabledAndUnchecked() { + when(mUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isFalse(); + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void afterQuietModeTurnedOnAndOffWhenPreferenceChecked_toggleCheckedAndEnabled() { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 1, MANAGED_USER_ID); + when(mUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isFalse(); + assertThat(mPreference.isChecked()).isFalse(); + + when(mUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isTrue(); + assertThat(mPreference.isChecked()).isTrue(); + } + + @Test + public void afterQuietModeTurnedOnAndOffWhenPreferenceUnchecked_toggleUncheckedAndEnabled() { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, MANAGED_USER_ID); + when(mUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isFalse(); + assertThat(mPreference.isChecked()).isFalse(); + + when(mUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false); + + mController.updateState(mPreference); + + assertThat(mPreference.isEnabled()).isTrue(); + assertThat(mPreference.isChecked()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/accounts/ManagedProfileQuietModeEnablerTest.java b/tests/robotests/src/com/android/settings/accounts/ManagedProfileQuietModeEnablerTest.java new file mode 100644 index 00000000000..2698efa4bd8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accounts/ManagedProfileQuietModeEnablerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accounts; + +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.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import java.util.Collections; + + +@RunWith(RobolectricTestRunner.class) +public class ManagedProfileQuietModeEnablerTest { + private static final int MANAGED_USER_ID = 10; + private Context mContext; + private ManagedProfileQuietModeEnabler mQuietModeEnabler; + private LifecycleOwner mLifecycleOwner = new LifecycleOwner() { + public LifecycleRegistry registry = new LifecycleRegistry(this); + + @Override + public Lifecycle getLifecycle() { + return registry; + } + }; + + @Mock + private ManagedProfileQuietModeEnabler.QuietModeChangeListener mOnQuietModeChangeListener; + @Mock + private UserManager mUserManager; + @Mock + private UserHandle mManagedUser; + @Mock + private UserInfo mUserInfo; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + when(mUserInfo.isManagedProfile()).thenReturn(true); + when(mUserManager.getUserInfo(anyInt())).thenReturn(mUserInfo); + when(mUserManager.getProcessUserId()).thenReturn(0); + when(mManagedUser.getIdentifier()).thenReturn(MANAGED_USER_ID); + when(mUserManager.getUserProfiles()).thenReturn(Collections.singletonList(mManagedUser)); + mQuietModeEnabler = new ManagedProfileQuietModeEnabler(mContext, + mOnQuietModeChangeListener); + } + + @Test + public void onSetQuietMode_shouldRequestQuietModeEnabled() { + mQuietModeEnabler.setQuietModeEnabled(false); + verify(mUserManager).requestQuietModeEnabled(false, mManagedUser); + mQuietModeEnabler.setQuietModeEnabled(true); + verify(mUserManager).requestQuietModeEnabled(true, mManagedUser); + } + + @Test + public void onIsQuietModeEnabled_shouldCallIsQuietModeEnabled() { + assertThat(mQuietModeEnabler.isQuietModeEnabled()).isEqualTo( + verify(mUserManager).isQuietModeEnabled(any())); + } + + @Test + public void onQuietModeChanged_listenerNotified() { + mQuietModeEnabler.onStart(mLifecycleOwner); + mContext.sendBroadcast(new Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE).putExtra( + Intent.EXTRA_USER_HANDLE, MANAGED_USER_ID)); + mContext.sendBroadcast(new Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE).putExtra( + Intent.EXTRA_USER_HANDLE, MANAGED_USER_ID)); + verify(mOnQuietModeChangeListener, times(2)).onQuietModeChanged(); + } + + @Test + public void onStart_shouldRegisterReceiver() { + mQuietModeEnabler.onStart(mLifecycleOwner); + verify(mContext).registerReceiver(eq(mQuietModeEnabler.mReceiver), any(), anyInt()); + } + + @Test + public void onStop_shouldUnregisterReceiver() { + // register it first + mContext.registerReceiver(mQuietModeEnabler.mReceiver, null, + Context.RECEIVER_EXPORTED/*UNAUDITED*/); + + mQuietModeEnabler.onStop(mLifecycleOwner); + verify(mContext).unregisterReceiver(mQuietModeEnabler.mReceiver); + } +} diff --git a/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java index 2a283181b1c..e862d108c7c 100644 --- a/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java @@ -19,18 +19,18 @@ 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.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.SwitchPreference; +import androidx.test.core.app.ApplicationProvider; -import com.android.settings.R; +import com.android.settingslib.widget.MainSwitchPreference; import org.junit.Before; import org.junit.Test; @@ -38,43 +38,51 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; + +import java.util.Collections; @RunWith(RobolectricTestRunner.class) public class WorkModePreferenceControllerTest { private static final String PREF_KEY = "work_mode"; + private static final int MANAGED_USER_ID = 10; + + private Context mContext; + private WorkModePreferenceController mController; + private MainSwitchPreference mPreference; @Mock private UserManager mUserManager; @Mock private UserHandle mManagedUser; - - private Context mContext; - private WorkModePreferenceController mController; - private SwitchPreference mPreference; + @Mock + private UserInfo mUserInfo; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); - when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); - + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); + mPreference = new MainSwitchPreference(mContext); + when(mUserInfo.isManagedProfile()).thenReturn(true); + when(mUserManager.getUserInfo(anyInt())).thenReturn(mUserInfo); + when(mUserManager.getProcessUserId()).thenReturn(0); + when(mUserManager.getUserProfiles()).thenReturn(Collections.singletonList(mManagedUser)); + when(mManagedUser.getIdentifier()).thenReturn(MANAGED_USER_ID); mController = new WorkModePreferenceController(mContext, PREF_KEY); - mController.setManagedUser(mManagedUser); - mPreference = new SwitchPreference(mContext); } @Test public void getAvailabilityStatus_noManagedUser_DISABLED() { - mController.setManagedUser(null); + when(mUserManager.getProcessUserId()).thenReturn(MANAGED_USER_ID); + mController = new WorkModePreferenceController(mContext, PREF_KEY); + assertThat(mController.getAvailabilityStatus()) .isNotEqualTo(WorkModePreferenceController.AVAILABLE); } @Test public void getAvailabilityStatus_hasManagedUser_AVAILABLE() { - mController.setManagedUser(mManagedUser); assertThat(mController.getAvailabilityStatus()) .isEqualTo(WorkModePreferenceController.AVAILABLE); } @@ -83,41 +91,29 @@ public class WorkModePreferenceControllerTest { public void updateState_shouldRefreshContent() { when(mUserManager.isQuietModeEnabled(any(UserHandle.class))) .thenReturn(false); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isTrue(); - assertThat(mPreference.getSummary()) - .isEqualTo(mContext.getText(R.string.work_mode_on_summary)); when(mUserManager.isQuietModeEnabled(any(UserHandle.class))) .thenReturn(true); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isFalse(); - assertThat(mPreference.getSummary()) - .isEqualTo(mContext.getText(R.string.work_mode_off_summary)); } @Test public void onPreferenceChange_shouldRequestQuietModeEnabled() { + mController.setPreference(mPreference); + mController.onPreferenceChange(mPreference, true); + verify(mUserManager).requestQuietModeEnabled(false, mManagedUser); mController.onPreferenceChange(mPreference, false); + verify(mUserManager).requestQuietModeEnabled(true, mManagedUser); } - - @Test - public void onStart_shouldRegisterReceiver() { - mController.onStart(); - verify(mContext).registerReceiver(eq(mController.mReceiver), any(), anyInt()); - } - - @Test - public void onStop_shouldUnregisterReceiver() { - // register it first - mContext.registerReceiver(mController.mReceiver, null, - Context.RECEIVER_EXPORTED/*UNAUDITED*/); - - mController.onStop(); - verify(mContext).unregisterReceiver(mController.mReceiver); - } } \ No newline at end of file From e4856156627ee327def08ba2bbe991e8c108550d Mon Sep 17 00:00:00 2001 From: Weng Su Date: Thu, 11 May 2023 21:53:26 +0800 Subject: [PATCH 07/21] Fix failing test cases in MobileNetworkSummaryControllerTest - Remove obsolete test items - Update the new callback interface Bug: 280044539 Bug: 280044731 Test: Manual test atest -c MobileNetworkSummaryControllerTest Change-Id: I8460c620c62981dd5ee3b280c1a7467c43dbceb1 --- .../MobileNetworkSummaryControllerTest.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java index 5310ae09801..8d6d2d9bc4e 100644 --- a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java +++ b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java @@ -133,18 +133,12 @@ public class MobileNetworkSummaryControllerTest { assertThat(mController.isAvailable()).isFalse(); } - @Ignore @Test - public void getSummary_noSubscriptions_correctSummaryAndClickHandler() { + public void getSummary_noSubscriptions_returnSummaryCorrectly() { mController.displayPreference(mPreferenceScreen); mController.onResume(); - assertThat(mController.getSummary()).isEqualTo("Add a network"); - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - assertThat(intentCaptor.getValue().getAction()).isEqualTo( - EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION); + assertThat(mController.getSummary()).isEqualTo("Add a network"); } @Test @@ -300,15 +294,13 @@ public class MobileNetworkSummaryControllerTest { assertThat(captor.getValue()).isFalse(); } - @Ignore @Test - public void onResume_noSubscriptionEsimDisabled_isDisabled() { + public void onAvailableSubInfoChanged_noSubscriptionEsimDisabled_isDisabled() { Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); - SubscriptionUtil.setAvailableSubscriptionsForTesting(null); when(mEuiccManager.isEnabled()).thenReturn(false); mController.displayPreference(mPreferenceScreen); - mController.onResume(); + mController.onAvailableSubInfoChanged(null); assertThat(mPreference.isEnabled()).isFalse(); } From 5656caecd01493679db68ea1937b9aa0f65b5233 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Thu, 11 May 2023 22:23:16 +0800 Subject: [PATCH 08/21] Correct shortcut summary to have different software types of shortcut result Root Cause: Did not consider different software types shortcut in AccessibilityShortcutPreferenceFragment Solution: Add support for different software shortcut types Bug: 263451053 Test: manual change shortcut type and check Change-Id: Ic425fdab790d4bbf1de6418ada382e420a9860fc --- ...ccessibilityShortcutPreferenceFragment.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java index 01158bf1e5b..b414addc41f 100644 --- a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java @@ -322,6 +322,18 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted getComponentName()); }; + private static CharSequence getSoftwareShortcutTypeSummary(Context context) { + int resId; + if (AccessibilityUtil.isFloatingMenuEnabled(context)) { + resId = R.string.accessibility_shortcut_edit_summary_software; + } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { + resId = R.string.accessibility_shortcut_edit_summary_software_gesture; + } else { + resId = R.string.accessibility_shortcut_edit_summary_software; + } + return context.getText(resId); + } + /** * This method will be invoked when a button in the tutorial dialog is clicked. * @@ -429,11 +441,9 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted getComponentName().flattenToString(), AccessibilityUtil.UserShortcutType.SOFTWARE); final List list = new ArrayList<>(); - final CharSequence softwareTitle = context.getText( - R.string.accessibility_shortcut_edit_summary_software); if (hasShortcutType(shortcutTypes, AccessibilityUtil.UserShortcutType.SOFTWARE)) { - list.add(softwareTitle); + list.add(getSoftwareShortcutTypeSummary(context)); } if (hasShortcutType(shortcutTypes, AccessibilityUtil.UserShortcutType.HARDWARE)) { final CharSequence hardwareTitle = context.getText( @@ -443,7 +453,7 @@ public abstract class AccessibilityShortcutPreferenceFragment extends Restricted // Show software shortcut if first time to use. if (list.isEmpty()) { - list.add(softwareTitle); + list.add(getSoftwareShortcutTypeSummary(context)); } return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */ From 5e2862f2e37c8fc561580ec41c4022df55520582 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Fri, 12 May 2023 01:10:19 +0800 Subject: [PATCH 09/21] [Settings] Avoid to unregister wrong observer Bug: 262345047 Test: atest passed. Change-Id: If324e8f35c15053b48320f86ff22759a6a68a62f --- .../slices/SlicePreferenceController.java | 17 +++++++++---- .../slices/SlicePreferenceControllerTest.java | 24 ++++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/slices/SlicePreferenceController.java b/src/com/android/settings/slices/SlicePreferenceController.java index df28304f738..eb10bd42be1 100644 --- a/src/com/android/settings/slices/SlicePreferenceController.java +++ b/src/com/android/settings/slices/SlicePreferenceController.java @@ -44,6 +44,7 @@ public class SlicePreferenceController extends BasePreferenceController implemen LiveData mLiveData; @VisibleForTesting SlicePreference mSlicePreference; + private boolean mIsObservering = false; private Uri mUri; public SlicePreferenceController(Context context, String preferenceKey) { @@ -68,25 +69,31 @@ public class SlicePreferenceController extends BasePreferenceController implemen }); //TODO(b/120803703): figure out why we need to remove observer first - mLiveData.removeObserver(this); + removeLiveDataObserver(); } @Override public void onStart() { - if (mLiveData != null) { + if (mLiveData != null && !mIsObservering) { + mIsObservering = true; mLiveData.observeForever(this); } } @Override public void onStop() { - if (mLiveData != null) { - mLiveData.removeObserver(this); - } + removeLiveDataObserver(); } @Override public void onChanged(Slice slice) { mSlicePreference.onSliceUpdated(slice); } + + private void removeLiveDataObserver() { + if (mLiveData != null && mIsObservering && mLiveData.hasActiveObservers()) { + mIsObservering = false; + mLiveData.removeObserver(this); + } + } } diff --git a/tests/robotests/src/com/android/settings/slices/SlicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/slices/SlicePreferenceControllerTest.java index 65eaddd50ab..e8bd27d97e6 100644 --- a/tests/robotests/src/com/android/settings/slices/SlicePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/slices/SlicePreferenceControllerTest.java @@ -18,8 +18,10 @@ package com.android.settings.slices; import static com.google.common.truth.Truth.assertThat; +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.content.Context; import android.net.Uri; @@ -43,11 +45,11 @@ public class SlicePreferenceControllerTest { private LiveData mLiveData; @Mock private SlicePreference mSlicePreference; + private Context mContext; private SlicePreferenceController mController; private Uri mUri; - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -78,10 +80,30 @@ public class SlicePreferenceControllerTest { @Test public void onStop_unregisterObserver() { + when(mLiveData.hasActiveObservers()).thenReturn(true); + mController.onStart(); + mController.onStop(); verify(mLiveData).removeObserver(mController); } + @Test + public void onStop_noActiveObservers_notUnregisterObserver() { + when(mLiveData.hasActiveObservers()).thenReturn(false); + mController.onStart(); + + mController.onStop(); + verify(mLiveData, never()).removeObserver(mController); + } + + @Test + public void onStop_notRegisterObserver_notUnregisterObserver() { + when(mLiveData.hasActiveObservers()).thenReturn(true); + + mController.onStop(); + verify(mLiveData, never()).removeObserver(mController); + } + @Test public void onChanged_nullSlice_updateSlice() { mController.onChanged(null); From cddf296c8255f12e178cad3afc4e40279a3c381b Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Wed, 10 May 2023 21:48:07 +0000 Subject: [PATCH 10/21] Hide description text view if there is overlap. 1. Includes udfps enroll view in xml files and cleans up FingerprintEnrollEnrolling adding udfps enroll view code. 2. Clean up SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS flag. 3. If description text view has overlap with udfps progress view, hide the description text view. Test: atest FingerprintEnrollEnrollingTest Test: manually test by setting both display and text largest size and start enrolling fingerprint; the description is hidden if it's too long. Bug: 260970216 Change-Id: I085dc62993ffa22d018dd57517c96d69e9d9cbcb --- res/layout-land/udfps_enroll_enrolling.xml | 2 + res/layout/udfps_enroll_enrolling.xml | 9 +- .../FingerprintEnrollEnrolling.java | 106 +++++++++--------- .../fingerprint/UdfpsEnrollView.java | 30 +++-- .../FingerprintEnrollEnrollingTest.java | 55 +++++---- 5 files changed, 110 insertions(+), 92 deletions(-) diff --git a/res/layout-land/udfps_enroll_enrolling.xml b/res/layout-land/udfps_enroll_enrolling.xml index f3237887e9a..743684fb02e 100644 --- a/res/layout-land/udfps_enroll_enrolling.xml +++ b/res/layout-land/udfps_enroll_enrolling.xml @@ -96,4 +96,6 @@ + + \ No newline at end of file diff --git a/res/layout/udfps_enroll_enrolling.xml b/res/layout/udfps_enroll_enrolling.xml index c97591d6d52..05556ffe46c 100644 --- a/res/layout/udfps_enroll_enrolling.xml +++ b/res/layout/udfps_enroll_enrolling.xml @@ -18,6 +18,7 @@ + android:layout_gravity="center_horizontal|bottom" + tools:ignore="Suspicious0dp"> + + + { + if (view.getVisibility() == View.VISIBLE + && hasOverlap(view, udfpsEnrollView)) { + view.setVisibility(View.GONE); + } + }); + setOnHoverListener(false, layout, udfpsEnrollView); setContentView(layout); break; case Surface.ROTATION_270: default: - if (FeatureFlagUtils.isEnabled(getApplicationContext(), - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) { - final UdfpsEnrollView udfpsEnrollView = addUdfpsEnrollView(props.get(0)); - layout.addView(udfpsEnrollView); - setOnHoverListener(true, layout, udfpsEnrollView); - } - + setOnHoverListener(true, layout, udfpsEnrollView); setContentView(layout); break; } @@ -1235,10 +1223,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { } } - private UdfpsEnrollView addUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps) { - UdfpsEnrollView udfpsEnrollView = (UdfpsEnrollView) getLayoutInflater().inflate( - R.layout.udfps_enroll_view, null, false); - + private UdfpsEnrollView updateUdfpsEnrollView(UdfpsEnrollView udfpsEnrollView, + FingerprintSensorPropertiesInternal udfpsProps) { DisplayInfo displayInfo = new DisplayInfo(); getDisplay().getDisplayInfo(displayInfo); mScaleFactor = mUdfpsUtils.getScaleFactor(displayInfo); @@ -1305,6 +1291,24 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { : R.id.sud_layout_content).setOnHoverListener(onHoverListener); } + + @VisibleForTesting boolean hasOverlap(View view1, View view2) { + int[] firstPosition = new int[2]; + int[] secondPosition = new int[2]; + + view1.getLocationOnScreen(firstPosition); + view2.getLocationOnScreen(secondPosition); + + // Rect constructor parameters: left, top, right, bottom + Rect rectView1 = new Rect(firstPosition[0], firstPosition[1], + firstPosition[0] + view1.getMeasuredWidth(), + firstPosition[1] + view1.getMeasuredHeight()); + Rect rectView2 = new Rect(secondPosition[0], secondPosition[1], + secondPosition[0] + view2.getMeasuredWidth(), + secondPosition[1] + view2.getMeasuredHeight()); + return rectView1.intersect(rectView2); + } + public static class IconTouchDialog extends InstrumentedDialogFragment { @Override diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java index 6e42059395f..5ded91ec68c 100644 --- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java @@ -161,22 +161,20 @@ public class UdfpsEnrollView extends FrameLayout implements UdfpsEnrollHelper.Li MarginLayoutParams marginLayoutParams = (MarginLayoutParams) getLayoutParams(); FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { - parentView.getViewTreeObserver().addOnDrawListener(() -> { - final int[] coords = parentView.getLocationOnScreen(); - final int parentLeft = coords[0]; - final int parentTop = coords[1]; - final int parentRight = parentLeft + parentView.getWidth(); - params.gravity = Gravity.RIGHT | Gravity.TOP; - final int rightMargin = parentRight - rotatedBounds.right - getPaddingX(); - final int topMargin = rotatedBounds.top - parentTop - getPaddingY(); - if (marginLayoutParams.rightMargin == rightMargin - && marginLayoutParams.topMargin == topMargin) { - return; - } - marginLayoutParams.rightMargin = rightMargin; - marginLayoutParams.topMargin = topMargin; - setLayoutParams(params); - }); + final int[] coords = parentView.getLocationOnScreen(); + final int parentLeft = coords[0]; + final int parentTop = coords[1]; + final int parentRight = parentLeft + parentView.getWidth(); + params.gravity = Gravity.RIGHT | Gravity.TOP; + final int rightMargin = parentRight - rotatedBounds.right - getPaddingX(); + final int topMargin = rotatedBounds.top - parentTop - getPaddingY(); + if (marginLayoutParams.rightMargin == rightMargin + && marginLayoutParams.topMargin == topMargin) { + return; + } + marginLayoutParams.rightMargin = rightMargin; + marginLayoutParams.topMargin = topMargin; + setLayoutParams(params); } else { final int[] coords = parentView.getLocationOnScreen(); final int parentLeft = coords[0]; diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java index 7282be357c1..959c6426894 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrollingTest.java @@ -53,7 +53,6 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Vibrator; -import android.util.FeatureFlagUtils; import android.view.Display; import android.view.Surface; import android.view.View; @@ -203,8 +202,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void fingerprintUdfpsOverlayEnrollment_showOverlayPortrait() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0); @@ -216,8 +213,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void fingerprintUdfpsOverlayEnrollment_showOverlayLandscape() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_90); @@ -229,8 +224,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void fingerprintUdfpsOverlayEnrollment_usesCorrectProgressBarFillColor() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); final TypedArray ta = mActivity.obtainStyledAttributes(null, R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle, @@ -250,9 +243,7 @@ public class FingerprintEnrollEnrollingTest { @Test public void fingerprintUdfpsOverlayEnrollment_checkViewOverlapPortrait() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); - when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_90); + when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0); initializeActivityFor(TYPE_UDFPS_OPTICAL); final GlifLayout defaultLayout = mActivity.findViewById(R.id.setup_wizard_layout); @@ -294,9 +285,9 @@ public class FingerprintEnrollEnrollingTest { udfpsEnrollView.getViewTreeObserver().addOnDrawListener(() -> { udfpsEnrollView.getLocationOnScreen(udfpsEnrollViewPosition); rectUdfpsEnrollView.set(new Rect(udfpsEnrollViewPosition[0], - udfpsEnrollViewPosition[1], udfpsEnrollViewPosition[0] - + udfpsEnrollView.getWidth(), udfpsEnrollViewPosition[1] - + udfpsEnrollView.getHeight())); + udfpsEnrollViewPosition[1], udfpsEnrollViewPosition[0] + + udfpsEnrollView.getWidth(), udfpsEnrollViewPosition[1] + + udfpsEnrollView.getHeight())); }); lottieAnimationContainer.getViewTreeObserver().addOnDrawListener(() -> { @@ -320,10 +311,36 @@ public class FingerprintEnrollEnrollingTest { .intersect(rectUdfpsEnrollView.get())).isFalse(); } + @Test + public void fingerprintUdfpsOverlayEnrollment_descriptionViewGoneWithOverlap() { + initializeActivityWithoutCreate(TYPE_UDFPS_OPTICAL); + doReturn(true).when(mActivity).hasOverlap(any(), any()); + when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0); + createActivity(); + + final GlifLayout defaultLayout = spy(mActivity.findViewById(R.id.setup_wizard_layout)); + final TextView descriptionTextView = defaultLayout.getDescriptionTextView(); + + defaultLayout.getViewTreeObserver().dispatchOnDraw(); + assertThat(descriptionTextView.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void fingerprintUdfpsOverlayEnrollment_descriptionViewVisibleWithoutOverlap() { + initializeActivityWithoutCreate(TYPE_UDFPS_OPTICAL); + doReturn(false).when(mActivity).hasOverlap(any(), any()); + when(mMockDisplay.getRotation()).thenReturn(Surface.ROTATION_0); + createActivity(); + + final GlifLayout defaultLayout = spy(mActivity.findViewById(R.id.setup_wizard_layout)); + final TextView descriptionTextView = defaultLayout.getDescriptionTextView(); + + defaultLayout.getViewTreeObserver().dispatchOnDraw(); + assertThat(descriptionTextView.getVisibility()).isEqualTo(View.VISIBLE); + } + @Test public void forwardEnrollProgressEvents() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); EnrollListener listener = new EnrollListener(mActivity); @@ -337,8 +354,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void forwardEnrollHelpEvents() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); EnrollListener listener = new EnrollListener(mActivity); @@ -352,8 +367,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void forwardEnrollAcquiredEvents() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); EnrollListener listener = new EnrollListener(mActivity); @@ -368,8 +381,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void forwardEnrollPointerDownEvents() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); EnrollListener listener = new EnrollListener(mActivity); @@ -383,8 +394,6 @@ public class FingerprintEnrollEnrollingTest { @Test public void forwardEnrollPointerUpEvents() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, true); initializeActivityFor(TYPE_UDFPS_OPTICAL); EnrollListener listener = new EnrollListener(mActivity); From f7c4d9c90350f29d51e40529c32817a597b3bca4 Mon Sep 17 00:00:00 2001 From: Becca Hughes Date: Thu, 11 May 2023 17:45:30 +0000 Subject: [PATCH 11/21] Fix string Test: make Bug: 281047738 Change-Id: I7f65749cbdee95ab9447dc0b055d66fd962547dc --- res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 56116b0bfc9..a2699ef77a2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10313,7 +10313,7 @@ Turn off this service?

- Save info like passwords, passkeys, payment methods, and other info won\'t be filled + Saved info like passwords, passkeys, payment methods, and other info won\'t be filled in when you sign in. To use your saved info, choose a password, passkey, or data service. ]]> From 715a70603c1161e6e8f18888df0c346040961b07 Mon Sep 17 00:00:00 2001 From: Yuxin Hu Date: Mon, 1 May 2023 21:48:33 +0000 Subject: [PATCH 12/21] Add AndroidJUnitTest for developer option switch Bug: b/270994705 Test: m -j45 atest SettingsRoboTests:GraphicsDriverEnableAngleAsSystemDriverControllerTest atest -c GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest Change-Id: Idbb93458a64894c3eba78a8f9373c40e3ddf35c5 --- ...erEnableAngleAsSystemDriverController.java | 47 ++- ...GraphicsDriverSystemPropertiesWrapper.java | 44 +++ ...ableAngleAsSystemDriverControllerTest.java | 22 +- ...ngleAsSystemDriverControllerJUnitTest.java | 325 ++++++++++++++++++ 4 files changed, 418 insertions(+), 20 deletions(-) create mode 100644 src/com/android/settings/development/graphicsdriver/GraphicsDriverSystemPropertiesWrapper.java create mode 100644 tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java diff --git a/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java b/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java index b9f34137111..04252fa0d56 100644 --- a/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java +++ b/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java @@ -48,6 +48,8 @@ public class GraphicsDriverEnableAngleAsSystemDriverController private final DevelopmentSettingsDashboardFragment mFragment; + private final GraphicsDriverSystemPropertiesWrapper mSystemProperties; + @VisibleForTesting static final String PROPERTY_RO_GFX_ANGLE_SUPPORTED = "ro.gfx.angle.supported"; @@ -57,11 +59,34 @@ public class GraphicsDriverEnableAngleAsSystemDriverController @VisibleForTesting static final String ANGLE_DRIVER_SUFFIX = "angle"; + @VisibleForTesting + static class Injector { + public GraphicsDriverSystemPropertiesWrapper createSystemPropertiesWrapper() { + return new GraphicsDriverSystemPropertiesWrapper() { + @Override + public String get(String key, String def) { + return SystemProperties.get(key, def); + } + + @Override + public void set(String key, String val) { + SystemProperties.set(key, val); + } + }; + } + } public GraphicsDriverEnableAngleAsSystemDriverController( Context context, DevelopmentSettingsDashboardFragment fragment) { + this(context, fragment, new Injector()); + } + + @VisibleForTesting + GraphicsDriverEnableAngleAsSystemDriverController( + Context context, DevelopmentSettingsDashboardFragment fragment, Injector injector) { super(context); mFragment = fragment; + mSystemProperties = injector.createSystemPropertiesWrapper(); } @Override @@ -76,20 +101,27 @@ public class GraphicsDriverEnableAngleAsSystemDriverController // set "persist.graphics.egl" to "" if enableAngleAsSystemDriver is false GraphicsEnvironment.getInstance().toggleAngleAsSystemDriver(enableAngleAsSystemDriver); // pop up a window asking user to reboot to make the new "persist.graphics.egl" take effect + showRebootDialog(); + return true; + } + + @VisibleForTesting + void showRebootDialog() { RebootConfirmationDialogFragment.show( mFragment, R.string.reboot_dialog_enable_angle_as_system_driver, R.string.cancel, this); - return true; } + @Override public void updateState(Preference preference) { // set switch on if "persist.graphics.egl" is "angle" and angle is built in /vendor // set switch off otherwise. - final String currentGlesDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + final String currentGlesDriver = + mSystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL, ""); final boolean isAngle = TextUtils.equals(ANGLE_DRIVER_SUFFIX, currentGlesDriver); - final boolean isAngleSupported = - TextUtils.equals(SystemProperties.get(PROPERTY_RO_GFX_ANGLE_SUPPORTED), "true"); + final boolean isAngleSupported = TextUtils + .equals(mSystemProperties.get(PROPERTY_RO_GFX_ANGLE_SUPPORTED, ""), "true"); ((SwitchPreference) mPreference).setChecked(isAngle && isAngleSupported); ((SwitchPreference) mPreference).setEnabled(isAngleSupported); } @@ -98,8 +130,8 @@ public class GraphicsDriverEnableAngleAsSystemDriverController protected void onDeveloperOptionsSwitchEnabled() { // only enable the switch if ro.gfx.angle.supported is true // we use ro.gfx.angle.supported to indicate if ANGLE libs are installed under /vendor - final boolean isAngleSupported = - TextUtils.equals(SystemProperties.get(PROPERTY_RO_GFX_ANGLE_SUPPORTED), "true"); + final boolean isAngleSupported = TextUtils + .equals(mSystemProperties.get(PROPERTY_RO_GFX_ANGLE_SUPPORTED, ""), "true"); ((SwitchPreference) mPreference).setEnabled(isAngleSupported); } @@ -116,7 +148,8 @@ public class GraphicsDriverEnableAngleAsSystemDriverController @Override public void onRebootCancelled() { // if user presses button "Cancel", do not reboot the device, and toggles switch back - final String currentGlesDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + final String currentGlesDriver = + mSystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL, ""); if (TextUtils.equals(ANGLE_DRIVER_SUFFIX, currentGlesDriver)) { // if persist.graphics.egl = "angle", set the property value back to "" GraphicsEnvironment.getInstance().toggleAngleAsSystemDriver(false); diff --git a/src/com/android/settings/development/graphicsdriver/GraphicsDriverSystemPropertiesWrapper.java b/src/com/android/settings/development/graphicsdriver/GraphicsDriverSystemPropertiesWrapper.java new file mode 100644 index 00000000000..549cd81c565 --- /dev/null +++ b/src/com/android/settings/development/graphicsdriver/GraphicsDriverSystemPropertiesWrapper.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.development.graphicsdriver; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.SystemProperties; +/** + * Wrapper interface to access {@link SystemProperties}. + * + * @hide + */ +interface GraphicsDriverSystemPropertiesWrapper { + /** + * Get the String value for the given {@code key}. + * + * @param key the key to lookup + * @param def the default value in case the property is not set or empty + * @return if the {@code key} isn't found, return {@code def} if it isn't null, or an empty + * string otherwise + */ + @NonNull + String get(@NonNull String key, @Nullable String def); + /** + * Set the value for the given {@code key} to {@code val}. + * + * @throws IllegalArgumentException if the {@code val} exceeds 91 characters + * @throws RuntimeException if the property cannot be set, for example, if it was blocked by + * SELinux. libc will log the underlying reason. + */ + void set(@NonNull String key, @Nullable String val); +} diff --git a/tests/robotests/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerTest.java b/tests/robotests/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerTest.java index 314f8c38091..de380c40c8e 100644 --- a/tests/robotests/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerTest.java +++ b/tests/robotests/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerTest.java @@ -81,9 +81,10 @@ public class GraphicsDriverEnableAngleAsSystemDriverControllerTest { // since GraphicsEnvironment is mocked in Robolectric test environment, // we will override the system property persist.graphics.egl as if it is changed by // mGraphicsEnvironment.toggleAngleAsSystemDriver(true). - // TODO: b/270994705 yuxinhu: - // add test coverage to test mGraphicsEnvironment.toggleAngleAsSystemDriver() - // works properly on Android devices / emulators. + + // for test that actually verifies mGraphicsEnvironment.toggleAngleAsSystemDriver(true) + // on a device/emulator, please refer to + // GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest ShadowSystemProperties.override(PROPERTY_PERSISTENT_GRAPHICS_EGL, ANGLE_DRIVER_SUFFIX); mController.onPreferenceChange(mPreference, true); final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); @@ -97,9 +98,10 @@ public class GraphicsDriverEnableAngleAsSystemDriverControllerTest { // since GraphicsEnvironment is mocked in Robolectric test environment, // we will override the system property persist.graphics.egl as if it is changed by // mGraphicsEnvironment.toggleAngleAsSystemDriver(false). - // TODO: b/270994705 yuxinhu: - // add test coverage to test mGraphicsEnvironment.toggleAngleAsSystemDriver() - // works properly on Android devices / emulators. + + // for test that actually verifies mGraphicsEnvironment.toggleAngleAsSystemDriver(true) + // on a device/emulator, please refer to + // GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest ShadowSystemProperties.override(PROPERTY_PERSISTENT_GRAPHICS_EGL, ""); mController.onPreferenceChange(mPreference, false); final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); @@ -124,20 +126,14 @@ public class GraphicsDriverEnableAngleAsSystemDriverControllerTest { @Test public void updateState_angleSupported_angleUsed_preferenceShouldBeChecked() { ShadowSystemProperties.override(PROPERTY_RO_GFX_ANGLE_SUPPORTED, "true"); - // TODO: b/270994705 yuxinhu: - // add test coverage to test mGraphicsEnvironment.toggleAngleAsSystemDriver() - // works properly on Android devices / emulators. ShadowSystemProperties.override(PROPERTY_PERSISTENT_GRAPHICS_EGL, ANGLE_DRIVER_SUFFIX); mController.updateState(mPreference); - verify(mPreference).setChecked(true); //false + verify(mPreference).setChecked(true); } @Test public void updateState_angleSupported_angleNotUsed_preferenceShouldNotBeChecked() { ShadowSystemProperties.override(PROPERTY_RO_GFX_ANGLE_SUPPORTED, "true"); - // TODO: b/270994705 yuxinhu: - // add test coverage to test mGraphicsEnvironment.toggleAngleAsSystemDriver(false) - // works properly on Android devices / emulators. ShadowSystemProperties.override(PROPERTY_PERSISTENT_GRAPHICS_EGL, ""); mController.updateState(mPreference); verify(mPreference).setChecked(false); diff --git a/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java b/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java new file mode 100644 index 00000000000..3f85535df53 --- /dev/null +++ b/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.graphicsdriver; + +import static com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverController.ANGLE_DRIVER_SUFFIX; +import static com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverController.Injector; +import static com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverController.PROPERTY_PERSISTENT_GRAPHICS_EGL; +import static com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverController.PROPERTY_RO_GFX_ANGLE_SUPPORTED; + +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.when; + +import android.content.Context; +import android.os.Looper; +import android.os.SystemProperties; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.development.DevelopmentSettingsDashboardFragment; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +public class GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest { + private Context mContext; + private SwitchPreference mPreference; + + private GraphicsDriverEnableAngleAsSystemDriverController mController; + + @Mock + private DevelopmentSettingsDashboardFragment mFragment; + + @Mock + private GraphicsDriverSystemPropertiesWrapper mSystemPropertiesMock; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + mContext = ApplicationProvider.getApplicationContext(); + + // Construct a GraphicsDriverEnableAngleAsSystemDriverController with two Overrides: + // 1) Override the mSystemProperties with mSystemPropertiesMock, + // so we can force the SystemProperties with values we need to run tests. + // 2) Override the showRebootDialog() to do nothing. + // We do not need to pop up the reboot dialog in the test. + mController = new GraphicsDriverEnableAngleAsSystemDriverController( + mContext, mFragment, new Injector(){ + @Override + public GraphicsDriverSystemPropertiesWrapper createSystemPropertiesWrapper() { + return mSystemPropertiesMock; + } + }) { + @Override + void showRebootDialog() { + // do nothing + } + }; + + final PreferenceManager preferenceManager = new PreferenceManager(mContext); + final PreferenceScreen screen = preferenceManager.createPreferenceScreen(mContext); + mPreference = new SwitchPreference(mContext); + mPreference.setKey(mController.getPreferenceKey()); + screen.addPreference(mPreference); + mController.displayPreference(screen); + } + + @Test + public void onPreferenceChange_switchOn_shouldEnableAngleAsSystemDriver() { + // Add a callback when SystemProperty changes. + // This allows the thread to wait until + // GpuService::toggleAngleAsSystemDriver() updates the persist.graphics.egl. + final CountDownLatch countDownLatch = new CountDownLatch(1); + Runnable countDown = new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }; + SystemProperties.addChangeCallback(countDown); + + // Test onPreferenceChange(true) updates the persist.graphics.egl to "angle" + mController.onPreferenceChange(mPreference, true); + try { + countDownLatch.await(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + assertThat(systemEGLDriver).isEqualTo(ANGLE_DRIVER_SUFFIX); + + // Done with the test, remove the callback + SystemProperties.removeChangeCallback(countDown); + } + + @Test + public void onPreferenceChange_switchOff_shouldDisableAngleAsSystemDriver() { + // Add a callback when SystemProperty changes. + // This allows the thread to wait until + // GpuService::toggleAngleAsSystemDriver() updates the persist.graphics.egl. + final CountDownLatch countDownLatch = new CountDownLatch(1); + Runnable countDown = new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }; + SystemProperties.addChangeCallback(countDown); + + // Test onPreferenceChange(false) updates the persist.graphics.egl to "" + mController.onPreferenceChange(mPreference, false); + try { + countDownLatch.await(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + assertThat(systemEGLDriver).isEqualTo(""); + + // Done with the test, remove the callback + SystemProperties.removeChangeCallback(countDown); + } + + @Test + public void updateState_angleNotSupported_PreferenceShouldDisabled() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())).thenReturn(""); + mController.updateState(mPreference); + assertThat(mPreference.isEnabled()).isFalse(); + } + + @Test + public void updateState_angleNotSupported_PreferenceShouldNotBeChecked() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())) + .thenReturn(""); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void updateState_angleSupported_PreferenceShouldEnabled() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())) + .thenReturn("true"); + mController.updateState(mPreference); + assertThat(mPreference.isEnabled()).isTrue(); + } + + @Test + public void updateState_angleSupported_angleIsSystemGLESDriver_PreferenceShouldBeChecked() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())) + .thenReturn("true"); + when(mSystemPropertiesMock.get(eq(PROPERTY_PERSISTENT_GRAPHICS_EGL), any())) + .thenReturn(ANGLE_DRIVER_SUFFIX); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isTrue(); + } + + @Test + public void + updateState_angleSupported_angleIsNotSystemGLESDriver_PreferenceShouldNotBeChecked() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())) + .thenReturn("true"); + when(mSystemPropertiesMock.get(eq(PROPERTY_PERSISTENT_GRAPHICS_EGL), any())) + .thenReturn(""); + mController.updateState(mPreference); + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void onDeveloperOptionSwitchEnabled_angleSupported_PreferenceShouldEnabled() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())) + .thenReturn("true"); + mController.onDeveloperOptionsSwitchEnabled(); + assertThat(mPreference.isEnabled()).isTrue(); + } + + @Test + public void onDeveloperOptionSwitchEnabled_angleNotSupported_PrefenceShouldDisabled() { + when(mSystemPropertiesMock.get(eq(PROPERTY_RO_GFX_ANGLE_SUPPORTED), any())) + .thenReturn("false"); + mController.onDeveloperOptionsSwitchEnabled(); + assertThat(mPreference.isEnabled()).isFalse(); + } + + @Test + public void onDeveloperOptionSwitchDisabled_angleIsNotSystemGLESDriver() { + // Add a callback when SystemProperty changes. + // This allows the thread to wait until + // GpuService::toggleAngleAsSystemDriver() updates the persist.graphics.egl. + final CountDownLatch countDownLatch = new CountDownLatch(1); + Runnable countDown = new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }; + SystemProperties.addChangeCallback(countDown); + + // Test that onDeveloperOptionSwitchDisabled, + // persist.graphics.egl updates to "" + mController.onDeveloperOptionsSwitchDisabled(); + try { + countDownLatch.await(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + assertThat(systemEGLDriver).isEqualTo(""); + + // Done with the test, remove the callback + SystemProperties.removeChangeCallback(countDown); + } + + @Test + public void onDeveloperOptionSwitchDisabled_PreferenceShouldNotBeChecked() { + mController.onDeveloperOptionsSwitchDisabled(); + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void onDeveloperOptionSwitchDisabled_PreferenceShouldDisabled() { + mController.onDeveloperOptionsSwitchDisabled(); + assertThat(mPreference.isEnabled()).isFalse(); + } + + @Test + public void onRebootCancelled_ToggleSwitchFromOnToOff() { + // Add a callback when SystemProperty changes. + // This allows the thread to wait until + // GpuService::toggleAngleAsSystemDriver() updates the persist.graphics.egl. + final CountDownLatch countDownLatch = new CountDownLatch(1); + Runnable countDown = new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }; + SystemProperties.addChangeCallback(countDown); + + // Test that if the current persist.graphics.egl is "angle", + // when reboot is cancelled, persist.graphics.egl is changed back to "", + // and switch is set to unchecked. + when(mSystemPropertiesMock.get(eq(PROPERTY_PERSISTENT_GRAPHICS_EGL), any())) + .thenReturn(ANGLE_DRIVER_SUFFIX); + mController.onRebootCancelled(); + try { + countDownLatch.await(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + + final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + assertThat(systemEGLDriver).isEqualTo(""); + assertThat(mPreference.isChecked()).isFalse(); + + // Done with the test, remove the callback. + SystemProperties.removeChangeCallback(countDown); + } + + @Test + public void onRebootCancelled_ToggleSwitchFromOffToOn() { + // Add a callback when SystemProperty changes. + // This allows the thread to wait until + // GpuService::toggleAngleAsSystemDriver() updates the persist.graphics.egl. + final CountDownLatch countDownLatch = new CountDownLatch(1); + Runnable countDown = new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }; + SystemProperties.addChangeCallback(countDown); + + // Test that if the current persist.graphics.egl is "", + // when reboot is cancelled, persist.graphics.egl is changed back to "angle", + // and switch is set to checked. + when(mSystemPropertiesMock.get(eq(PROPERTY_PERSISTENT_GRAPHICS_EGL), any())) + .thenReturn(""); + mController.onRebootCancelled(); + try { + countDownLatch.await(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } + + final String systemEGLDriver = SystemProperties.get(PROPERTY_PERSISTENT_GRAPHICS_EGL); + assertThat(systemEGLDriver).isEqualTo(ANGLE_DRIVER_SUFFIX); + assertThat(mPreference.isChecked()).isTrue(); + + // Done with the test, remove the callback. + SystemProperties.removeChangeCallback(countDown); + } + +} From fb6b6b0bcccd7e47a37aea01a76a597aef0176d9 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Tue, 2 May 2023 08:47:51 +0000 Subject: [PATCH 13/21] [Panlingual] Do not show dialog in recycle. Set LocaleEditor as the parent fragment and control the dialog. Keep the dialog and the locale list after rotation Bug: 279527362 Change-Id: I349fd9d0fea5b43a8bbb2a23fa60fc4c5436c5cf Test: make RunSettingsRoboTests -j128 ROBOTEST_FILTER=LocaleListEditorTest Test: atest LocaleDialogFragmentTest --- .../localepicker/LocaleDialogFragment.java | 107 +++++++++--------- .../LocaleDragAndDropAdapter.java | 95 +++++++--------- .../localepicker/LocaleListEditor.java | 64 ++++++++++- .../localepicker/LocaleRecyclerView.java | 11 -- .../localepicker/LocaleListEditorTest.java | 100 +++++++++++++++- .../LocaleDialogFragmentTest.java | 57 +++------- 6 files changed, 266 insertions(+), 168 deletions(-) diff --git a/src/com/android/settings/localepicker/LocaleDialogFragment.java b/src/com/android/settings/localepicker/LocaleDialogFragment.java index 2dc09bd9ee0..ad9e10f19ed 100644 --- a/src/com/android/settings/localepicker/LocaleDialogFragment.java +++ b/src/com/android/settings/localepicker/LocaleDialogFragment.java @@ -21,8 +21,8 @@ import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; -import android.os.ResultReceiver; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -35,7 +35,6 @@ import androidx.fragment.app.FragmentManager; import com.android.internal.app.LocaleStore; import com.android.settings.R; -import com.android.settings.RestrictedSettingsFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -46,49 +45,19 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class LocaleDialogFragment extends InstrumentedDialogFragment { private static final String TAG = LocaleDialogFragment.class.getSimpleName(); - static final int DIALOG_CONFIRM_SYSTEM_DEFAULT = 0; - static final int DIALOG_NOT_AVAILABLE_LOCALE = 1; + static final int DIALOG_CONFIRM_SYSTEM_DEFAULT = 1; + static final int DIALOG_NOT_AVAILABLE_LOCALE = 2; static final String ARG_DIALOG_TYPE = "arg_dialog_type"; static final String ARG_TARGET_LOCALE = "arg_target_locale"; - static final String ARG_RESULT_RECEIVER = "arg_result_receiver"; + static final String ARG_SHOW_DIALOG = "arg_show_dialog"; + + private boolean mShouldKeepDialog; public static LocaleDialogFragment newInstance() { return new LocaleDialogFragment(); } - /** - * Show dialog - */ - public void show( - @NonNull RestrictedSettingsFragment fragment, - int dialogType, - LocaleStore.LocaleInfo localeInfo) { - if (!isAdded()) { - return; - } - show(fragment, dialogType, localeInfo, null); - } - - /** - * Show dialog - */ - public void show( - @NonNull RestrictedSettingsFragment fragment, - int dialogType, - LocaleStore.LocaleInfo localeInfo, - ResultReceiver resultReceiver) { - FragmentManager manager = fragment.getChildFragmentManager(); - Bundle args = new Bundle(); - args.putInt(ARG_DIALOG_TYPE, dialogType); - args.putSerializable(ARG_TARGET_LOCALE, localeInfo); - args.putParcelable(ARG_RESULT_RECEIVER, resultReceiver); - - LocaleDialogFragment localeDialogFragment = new LocaleDialogFragment(); - localeDialogFragment.setArguments(args); - localeDialogFragment.show(manager, TAG); - } - @Override public int getMetricsCategory() { int dialogType = getArguments().getInt(ARG_DIALOG_TYPE); @@ -102,9 +71,29 @@ public class LocaleDialogFragment extends InstrumentedDialogFragment { } } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(ARG_SHOW_DIALOG, mShouldKeepDialog); + } + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - LocaleDialogController controller = new LocaleDialogController(this); + if (savedInstanceState != null) { + Bundle arguments = getArguments(); + int type = arguments.getInt(ARG_DIALOG_TYPE); + mShouldKeepDialog = savedInstanceState.getBoolean(ARG_SHOW_DIALOG, false); + // Keep the dialog if user rotates the device, otherwise close the confirm system + // default dialog only when user changes the locale. + if (type == DIALOG_CONFIRM_SYSTEM_DEFAULT && !mShouldKeepDialog) { + dismiss(); + } + } + + mShouldKeepDialog = true; + LocaleListEditor parentFragment = (LocaleListEditor) getParentFragment(); + LocaleDialogController controller = getLocaleDialogController(getContext(), this, + parentFragment); LocaleDialogController.DialogContent dialogContent = controller.getDialogContent(); ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(getContext()).inflate( R.layout.locale_dialog, null); @@ -140,47 +129,57 @@ public class LocaleDialogFragment extends InstrumentedDialogFragment { textView.setText(content); } - static class LocaleDialogController implements DialogInterface.OnClickListener { + @VisibleForTesting + LocaleDialogController getLocaleDialogController(Context context, + LocaleDialogFragment dialogFragment, LocaleListEditor parentFragment) { + return new LocaleDialogController(context, dialogFragment, parentFragment); + } + + class LocaleDialogController implements DialogInterface.OnClickListener { private final Context mContext; private final int mDialogType; private final LocaleStore.LocaleInfo mLocaleInfo; - private final ResultReceiver mResultReceiver; private final MetricsFeatureProvider mMetricsFeatureProvider; + private LocaleListEditor mParent; + LocaleDialogController( - @NonNull Context context, @NonNull LocaleDialogFragment dialogFragment) { + @NonNull Context context, @NonNull LocaleDialogFragment dialogFragment, + LocaleListEditor parentFragment) { mContext = context; Bundle arguments = dialogFragment.getArguments(); mDialogType = arguments.getInt(ARG_DIALOG_TYPE); - mLocaleInfo = (LocaleStore.LocaleInfo) arguments.getSerializable( - ARG_TARGET_LOCALE); - mResultReceiver = (ResultReceiver) arguments.getParcelable(ARG_RESULT_RECEIVER); + mLocaleInfo = (LocaleStore.LocaleInfo) arguments.getSerializable(ARG_TARGET_LOCALE); mMetricsFeatureProvider = FeatureFactory.getFactory( mContext).getMetricsFeatureProvider(); + mParent = parentFragment; } - LocaleDialogController(@NonNull LocaleDialogFragment dialogFragment) { - this(dialogFragment.getContext(), dialogFragment); + LocaleDialogController(@NonNull LocaleDialogFragment dialogFragment, + LocaleListEditor parent) { + this(dialogFragment.getContext(), dialogFragment, parent); } @Override public void onClick(DialogInterface dialog, int which) { - if (mResultReceiver != null && mDialogType == DIALOG_CONFIRM_SYSTEM_DEFAULT) { + if (mDialogType == DIALOG_CONFIRM_SYSTEM_DEFAULT) { + int result = Activity.RESULT_CANCELED; + if (which == DialogInterface.BUTTON_POSITIVE) { + result = Activity.RESULT_OK; + } + Intent intent = new Intent(); Bundle bundle = new Bundle(); bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); - if (which == DialogInterface.BUTTON_POSITIVE) { - mResultReceiver.send(Activity.RESULT_OK, bundle); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - mResultReceiver.send(Activity.RESULT_CANCELED, bundle); - } + intent.putExtras(bundle); + mParent.onActivityResult(DIALOG_CONFIRM_SYSTEM_DEFAULT, result, intent); mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_CHANGE_LANGUAGE); } + mShouldKeepDialog = false; } @VisibleForTesting DialogContent getDialogContent() { - DialogContent - dialogContent = new DialogContent(); + DialogContent dialogContent = new DialogContent(); switch (mDialogType) { case DIALOG_CONFIRM_SYSTEM_DEFAULT: dialogContent.mTitle = String.format(mContext.getString( diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index 2223db06562..edd302645e2 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -16,14 +16,11 @@ package com.android.settings.localepicker; -import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.os.Bundle; -import android.os.Handler; import android.os.LocaleList; -import android.os.Looper; -import android.os.ResultReceiver; +import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; @@ -52,15 +49,20 @@ class LocaleDragAndDropAdapter private static final String TAG = "LocaleDragAndDropAdapter"; private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales"; + private static final String CFGKEY_DRAG_LOCALE = "dragLocales"; + private static final String CFGKEY_DRAG_LOCALES_TO_POSITION = "dragLocales_end"; + private final Context mContext; + private final ItemTouchHelper mItemTouchHelper; + private List mFeedItemList; private List mCacheItemList; - private final ItemTouchHelper mItemTouchHelper; private RecyclerView mParentView = null; private LocaleListEditor mParent; private boolean mRemoveMode = false; private boolean mDragEnabled = true; private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance(); + private LocaleStore.LocaleInfo mDragLocale; class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener { private final LocaleDragCell mLocaleDragCell; @@ -87,8 +89,7 @@ class LocaleDragAndDropAdapter } } - LocaleDragAndDropAdapter(LocaleListEditor parent, - List feedItemList) { + LocaleDragAndDropAdapter(LocaleListEditor parent, List feedItemList) { mFeedItemList = feedItemList; mParent = parent; mCacheItemList = new ArrayList<>(feedItemList); @@ -202,6 +203,7 @@ class LocaleDragAndDropAdapter final LocaleStore.LocaleInfo saved = mFeedItemList.get(fromPosition); mFeedItemList.remove(fromPosition); mFeedItemList.add(toPosition, saved); + mDragLocale = saved; } else { // TODO: It looks like sometimes the RecycleView tries to swap item -1 // I did not see it in a while, but if it happens, investigate and file a bug. @@ -317,43 +319,20 @@ class LocaleDragAndDropAdapter }); } - public void doTheUpdateWithMovingLocaleItem() { - LocaleStore.LocaleInfo localeInfo = mFeedItemList.get(0); - final LocaleDialogFragment fragment = LocaleDialogFragment.newInstance(); - if (!localeInfo.getLocale().equals(LocalePicker.getLocales().get(0))) { - fragment.show(mParent, - LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, - localeInfo, - new ResultReceiver(new Handler(Looper.getMainLooper())) { - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - super.onReceiveResult(resultCode, resultData); - int type = resultData.getInt(LocaleDialogFragment.ARG_DIALOG_TYPE); - if (type == LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT) { - if (resultCode == Activity.RESULT_OK) { - doTheUpdate(); - if (!localeInfo.isTranslated()) { - fragment.show(mParent, - LocaleDialogFragment - .DIALOG_NOT_AVAILABLE_LOCALE, - localeInfo); - } - } else { - if (!localeInfo.getLocale() - .equals(mCacheItemList.get(0).getLocale())) { - mFeedItemList = new ArrayList<>(mCacheItemList); - notifyDataSetChanged(); - } - } - mCacheItemList = new ArrayList<>(mFeedItemList); - } - } - }); - } else { - doTheUpdate(); + public void notifyListChanged(LocaleStore.LocaleInfo localeInfo) { + if (!localeInfo.getLocale().equals(mCacheItemList.get(0).getLocale())) { + mFeedItemList = new ArrayList<>(mCacheItemList); + notifyDataSetChanged(); } } + public void setCacheItemList() { + mCacheItemList = new ArrayList<>(mFeedItemList); + } + + public List getFeedItemList() { + return mFeedItemList; + } private void setDragEnabled(boolean enabled) { mDragEnabled = enabled; } @@ -373,6 +352,8 @@ class LocaleDragAndDropAdapter } } outInstanceState.putStringArrayList(CFGKEY_SELECTED_LOCALES, selectedLocales); + // Save the dragged locale before rotation + outInstanceState.putSerializable(CFGKEY_DRAG_LOCALE, mDragLocale); } } @@ -381,18 +362,30 @@ class LocaleDragAndDropAdapter * (for instance when the device is rotated) * * @param savedInstanceState Bundle with the data saved by {@link #saveState(Bundle)} + * @param isDialogShowing A flag indicating whether the dialog is showing or not. */ - public void restoreState(Bundle savedInstanceState) { - if (savedInstanceState != null && mRemoveMode) { - final ArrayList selectedLocales = - savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES); - if (selectedLocales == null || selectedLocales.isEmpty()) { - return; + public void restoreState(Bundle savedInstanceState, boolean isDialogShowing) { + if (savedInstanceState != null) { + if (mRemoveMode) { + final ArrayList selectedLocales = + savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES); + if (selectedLocales == null || selectedLocales.isEmpty()) { + return; + } + for (LocaleStore.LocaleInfo li : mFeedItemList) { + li.setChecked(selectedLocales.contains(li.getId())); + } + notifyItemRangeChanged(0, mFeedItemList.size()); + } else if (isDialogShowing) { + // After rotation, the dragged position will be restored to original. Restore the + // drag locale's original position to the top. + mDragLocale = (LocaleStore.LocaleInfo) savedInstanceState.getSerializable( + CFGKEY_DRAG_LOCALE); + mFeedItemList.removeIf( + localeInfo -> TextUtils.equals(localeInfo.getId(), mDragLocale.getId())); + mFeedItemList.add(0, mDragLocale); + notifyItemRangeChanged(0, mFeedItemList.size()); } - for (LocaleStore.LocaleInfo li : mFeedItemList) { - li.setChecked(selectedLocales.contains(li.getId())); - } - notifyItemRangeChanged(0, mFeedItemList.size()); } } } diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index 6317f24a304..7ec08f7300e 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -18,6 +18,8 @@ package com.android.settings.localepicker; import static android.os.UserManager.DISALLOW_CONFIG_LOCALE; +import static com.android.settings.localepicker.LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT; + import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; @@ -32,12 +34,14 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; @@ -60,7 +64,7 @@ import java.util.Locale; * Drag-and-drop editor for the user-ordered locale lists. */ @SearchIndexable -public class LocaleListEditor extends RestrictedSettingsFragment { +public class LocaleListEditor extends RestrictedSettingsFragment implements View.OnTouchListener { protected static final String INTENT_LOCALE_KEY = "localeInfo"; private static final String CFGKEY_REMOVE_MODE = "localeRemoveMode"; @@ -70,6 +74,8 @@ public class LocaleListEditor extends RestrictedSettingsFragment { private static final String INDEX_KEY_ADD_LANGUAGE = "add_language"; private static final String KEY_LANGUAGES_PICKER = "languages_picker"; + 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 LocaleDragAndDropAdapter mAdapter; private Menu mMenu; @@ -80,6 +86,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment { private LayoutPreference mLocalePickerPreference; private LocaleHelperPreferenceController mLocaleHelperPreferenceController; + private FragmentManager mFragmentManager; public LocaleListEditor() { super(DISALLOW_CONFIG_LOCALE); @@ -106,6 +113,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment { LocaleStore.fillCache(this.getContext()); final List feedsList = getUserLocaleList(); mAdapter = new LocaleDragAndDropAdapter(this, feedsList); + mFragmentManager = getChildFragmentManager(); } @Override @@ -141,7 +149,15 @@ public class LocaleListEditor extends RestrictedSettingsFragment { mShowingRemoveDialog = savedInstanceState.getBoolean(CFGKEY_REMOVE_DIALOG, false); } setRemoveMode(mRemoveMode); - mAdapter.restoreState(savedInstanceState); + + final LocaleDialogFragment dialogFragment = + (LocaleDialogFragment) mFragmentManager.findFragmentByTag( + TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT); + boolean isDialogShowing = false; + if (dialogFragment != null && dialogFragment.isAdded()) { + isDialogShowing = true; + } + mAdapter.restoreState(savedInstanceState, isDialogShowing); if (mShowingRemoveDialog) { showRemoveLocaleWarningDialog(); @@ -178,17 +194,32 @@ public class LocaleListEditor extends RestrictedSettingsFragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + LocaleStore.LocaleInfo localeInfo; if (requestCode == REQUEST_LOCALE_PICKER && resultCode == Activity.RESULT_OK && data != null) { - final LocaleStore.LocaleInfo localeInfo = - (LocaleStore.LocaleInfo) data.getSerializableExtra( - INTENT_LOCALE_KEY); - + localeInfo = (LocaleStore.LocaleInfo) data.getSerializableExtra(INTENT_LOCALE_KEY); String preferencesTags = Settings.System.getString( getContext().getContentResolver(), Settings.System.LOCALE_PREFERENCES); mAdapter.addLocale(mayAppendUnicodeTags(localeInfo, preferencesTags)); updateVisibilityOfRemoveMenu(); + } else if (requestCode == DIALOG_CONFIRM_SYSTEM_DEFAULT) { + localeInfo = mAdapter.getFeedItemList().get(0); + if (resultCode == Activity.RESULT_OK) { + mAdapter.doTheUpdate(); + if (!localeInfo.isTranslated()) { + Bundle args = new Bundle(); + args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, + LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE); + args.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE, localeInfo); + LocaleDialogFragment localeDialogFragment = LocaleDialogFragment.newInstance(); + localeDialogFragment.setArguments(args); + localeDialogFragment.show(mFragmentManager, TAG_DIALOG_NOT_AVAILABLE); + } + } else { + mAdapter.notifyListChanged(localeInfo); + } + mAdapter.setCacheItemList(); } super.onActivityResult(requestCode, resultCode, data); } @@ -332,6 +363,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment { list.setNestedScrollingEnabled(false); mAdapter.setRecyclerView(list); list.setAdapter(mAdapter); + list.setOnTouchListener(this); mAddLanguage = layout.findViewById(R.id.add_language); mAddLanguage.setOnClickListener(new View.OnClickListener() { @@ -348,6 +380,26 @@ public class LocaleListEditor extends RestrictedSettingsFragment { }); } + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + LocaleStore.LocaleInfo localeInfo = mAdapter.getFeedItemList().get(0); + if (!localeInfo.getLocale().equals(LocalePicker.getLocales().get(0))) { + 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, localeInfo); + localeDialogFragment.setArguments(args); + localeDialogFragment.show(mFragmentManager, TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT); + } else { + mAdapter.doTheUpdate(); + } + } + return false; + } + // 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/src/com/android/settings/localepicker/LocaleRecyclerView.java b/src/com/android/settings/localepicker/LocaleRecyclerView.java index 5d469bf7f10..4a5f28b8855 100644 --- a/src/com/android/settings/localepicker/LocaleRecyclerView.java +++ b/src/com/android/settings/localepicker/LocaleRecyclerView.java @@ -34,15 +34,4 @@ class LocaleRecyclerView extends RecyclerView { public LocaleRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - - @Override - public boolean onTouchEvent(MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) { - LocaleDragAndDropAdapter adapter = (LocaleDragAndDropAdapter) this.getAdapter(); - if (adapter != null) { - adapter.doTheUpdateWithMovingLocaleItem(); - } - } - return super.onTouchEvent(e); - } } diff --git a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java index 111ee5a7571..16d51beca64 100644 --- a/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java +++ b/tests/robotests/src/com/android/settings/localepicker/LocaleListEditorTest.java @@ -18,19 +18,32 @@ package com.android.settings.localepicker; 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.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Activity; +import android.app.IActivityManager; import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.LocaleList; +import android.view.MotionEvent; import android.view.View; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import com.android.internal.app.LocaleStore; import com.android.settings.R; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowActivityManager; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import org.junit.After; @@ -45,22 +58,42 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; @RunWith(RobolectricTestRunner.class) -@Config(shadows = ShadowAlertDialogCompat.class) +@Config(shadows = {ShadowAlertDialogCompat.class, ShadowActivityManager.class}) public class LocaleListEditorTest { + private static final String ARG_DIALOG_TYPE = "arg_dialog_type"; + 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 int DIALOG_CONFIRM_SYSTEM_DEFAULT = 1; + private static final int REQUEST_CONFIRM_SYSTEM_DEFAULT = 1; + private LocaleListEditor mLocaleListEditor; private Context mContext; private FragmentActivity mActivity; + private List mLocaleList; + private Intent mIntent = new Intent(); @Mock private LocaleDragAndDropAdapter mAdapter; + @Mock + private LocaleStore.LocaleInfo mLocaleInfo; + @Mock + private FragmentManager mFragmentManager; + @Mock + private FragmentTransaction mFragmentTransaction; + @Mock + private View mView; + @Mock + private IActivityManager mActivityService; @Before - public void setUp() { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; mLocaleListEditor = spy(new LocaleListEditor()); @@ -74,6 +107,8 @@ public class LocaleListEditorTest { ReflectionHelpers.setField(mLocaleListEditor, "mUserManager", RuntimeEnvironment.application.getSystemService(Context.USER_SERVICE)); ReflectionHelpers.setField(mLocaleListEditor, "mAdapter", mAdapter); + ReflectionHelpers.setField(mLocaleListEditor, "mFragmentManager", mFragmentManager); + when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction); FakeFeatureFactory.setupForTest(); } @@ -174,4 +209,65 @@ public class LocaleListEditorTest { assertThat(result.getLocale().getUnicodeLocaleType("fw")).isEqualTo("wed"); assertThat(result.getLocale().getUnicodeLocaleType("mu")).isEqualTo("celsius"); } + + @Test + public void onActivityResult_ResultCodeIsOk_showNotAvailableDialog() { + Bundle bundle = new Bundle(); + bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); + mIntent.putExtras(bundle); + setUpLocaleConditions(); + mLocaleListEditor.onActivityResult(REQUEST_CONFIRM_SYSTEM_DEFAULT, Activity.RESULT_OK, + mIntent); + + verify(mFragmentTransaction).add(any(LocaleDialogFragment.class), + eq(TAG_DIALOG_NOT_AVAILABLE)); + } + + @Test + public void onActivityResult_ResultCodeIsCancel_notifyAdapterListChanged() { + Bundle bundle = new Bundle(); + bundle.putInt(ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT); + mIntent.putExtras(bundle); + setUpLocaleConditions(); + mLocaleListEditor.onActivityResult(REQUEST_CONFIRM_SYSTEM_DEFAULT, Activity.RESULT_CANCELED, + mIntent); + + verify(mAdapter).notifyListChanged(mLocaleInfo); + } + + @Test + public void onTouch_dragDifferentLocaleToTop_showConfirmDialog() throws Exception { + MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0); + setUpLocaleConditions(); + final Configuration config = new Configuration(); + config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US"))); + when(mActivityService.getConfiguration()).thenReturn(config); + when(mAdapter.getFeedItemList()).thenReturn(mLocaleList); + mLocaleListEditor.onTouch(mView, event); + + verify(mFragmentTransaction).add(any(LocaleDialogFragment.class), + eq(TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT)); + } + + @Test + public void onTouch_dragSameLocaleToTop_updateAdapter() throws Exception { + MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0); + setUpLocaleConditions(); + final Configuration config = new Configuration(); + config.setLocales((LocaleList.forLanguageTags("en-US,zh-TW"))); + when(mActivityService.getConfiguration()).thenReturn(config); + when(mAdapter.getFeedItemList()).thenReturn(mLocaleList); + mLocaleListEditor.onTouch(mView, event); + + verify(mAdapter).doTheUpdate(); + } + + private void setUpLocaleConditions() { + 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(mAdapter.getFeedItemList()).thenReturn(mLocaleList); + } } diff --git a/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java b/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java index b0998c023a6..824954da52d 100644 --- a/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java +++ b/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java @@ -17,8 +17,8 @@ package com.android.settings.localepicker; import static com.android.settings.localepicker.LocaleDialogFragment.ARG_DIALOG_TYPE; -import static com.android.settings.localepicker.LocaleDialogFragment.ARG_RESULT_RECEIVER; import static com.android.settings.localepicker.LocaleDialogFragment.ARG_TARGET_LOCALE; +import static com.android.settings.localepicker.LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -27,12 +27,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; -import android.os.ResultReceiver; import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ApplicationProvider; @@ -55,6 +52,7 @@ public class LocaleDialogFragmentTest { public final MockitoRule mockito = MockitoJUnit.rule(); private Context mContext; + private LocaleListEditor mLocaleListEditor; private LocaleDialogFragment mDialogFragment; private FakeFeatureFactory mFeatureFactory; @@ -62,30 +60,30 @@ public class LocaleDialogFragmentTest { public void setUp() throws Exception { mContext = ApplicationProvider.getApplicationContext(); mDialogFragment = new LocaleDialogFragment(); + mLocaleListEditor = spy(new LocaleListEditor()); mFeatureFactory = FakeFeatureFactory.setupForTest(); } - private void setArgument( - int type, ResultReceiver receiver) { + private void setArgument(int type) { LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(Locale.ENGLISH); Bundle args = new Bundle(); args.putInt(ARG_DIALOG_TYPE, type); args.putSerializable(ARG_TARGET_LOCALE, localeInfo); - args.putParcelable(ARG_RESULT_RECEIVER, receiver); mDialogFragment.setArguments(args); } @Test public void getDialogContent_confirmSystemDefault_has2ButtonText() { - setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, null); + setArgument(DIALOG_CONFIRM_SYSTEM_DEFAULT); LocaleDialogFragment.LocaleDialogController controller = - new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); + mDialogFragment.getLocaleDialogController(mContext, mDialogFragment, + mLocaleListEditor); LocaleDialogFragment.LocaleDialogController.DialogContent dialogContent = controller.getDialogContent(); assertEquals(ResourcesUtils.getResourcesString( - mContext, "button_label_confirmation_of_system_locale_change"), + mContext, "button_label_confirmation_of_system_locale_change"), dialogContent.mPositiveButton); assertEquals(ResourcesUtils.getResourcesString(mContext, "cancel"), dialogContent.mNegativeButton); @@ -93,9 +91,10 @@ public class LocaleDialogFragmentTest { @Test public void getDialogContent_unavailableLocale_has1ButtonText() { - setArgument(LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE, null); + setArgument(LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE); LocaleDialogFragment.LocaleDialogController controller = - new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); + mDialogFragment.getLocaleDialogController(mContext, mDialogFragment, + mLocaleListEditor); LocaleDialogFragment.LocaleDialogController.DialogContent dialogContent = controller.getDialogContent(); @@ -105,38 +104,9 @@ public class LocaleDialogFragmentTest { assertTrue(dialogContent.mNegativeButton.isEmpty()); } - @Test - public void onClick_clickPositiveButton_sendOK() { - ResultReceiver resultReceiver = spy(new ResultReceiver(null)); - setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, resultReceiver); - LocaleDialogFragment.LocaleDialogController controller = - new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); - - controller.onClick(null, DialogInterface.BUTTON_POSITIVE); - - verify(resultReceiver).send(eq(Activity.RESULT_OK), any()); - verify(mFeatureFactory.metricsFeatureProvider).action( - mContext, SettingsEnums.ACTION_CHANGE_LANGUAGE, true); - } - - @Test - public void onClick_clickNegativeButton_sendCancel() { - ResultReceiver resultReceiver = spy(new ResultReceiver(null)); - setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, resultReceiver); - LocaleDialogFragment.LocaleDialogController controller = - new LocaleDialogFragment.LocaleDialogController(mContext, mDialogFragment); - - controller.onClick(null, DialogInterface.BUTTON_NEGATIVE); - - verify(resultReceiver).send(eq(Activity.RESULT_CANCELED), any()); - verify(mFeatureFactory.metricsFeatureProvider).action( - mContext, SettingsEnums.ACTION_CHANGE_LANGUAGE, false); - } - @Test public void getMetricsCategory_systemLocaleChange() { - setArgument(LocaleDialogFragment.DIALOG_CONFIRM_SYSTEM_DEFAULT, null); - + setArgument(DIALOG_CONFIRM_SYSTEM_DEFAULT); int result = mDialogFragment.getMetricsCategory(); assertEquals(SettingsEnums.DIALOG_SYSTEM_LOCALE_CHANGE, result); @@ -144,8 +114,7 @@ public class LocaleDialogFragmentTest { @Test public void getMetricsCategory_unavailableLocale() { - setArgument(LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE, null); - + setArgument(LocaleDialogFragment.DIALOG_NOT_AVAILABLE_LOCALE); int result = mDialogFragment.getMetricsCategory(); assertEquals(SettingsEnums.DIALOG_SYSTEM_LOCALE_UNAVAILABLE, result); From 8e93ac66709d611c8d8a2e5cb7d27fe28849248c Mon Sep 17 00:00:00 2001 From: Chris Antol Date: Fri, 12 May 2023 03:01:24 +0000 Subject: [PATCH 14/21] Remove SPA gating from App Cloning click ingress Bug: 277002988 Test: existing tests. flag on by default, no-op unless disabled, which now has access to this otherwise accessible feature Change-Id: Ic37e4c76d379133f1dbda3c65930ae44afc24f70 --- .../applications/manageapplications/ManageApplications.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index a6e6ac47673..e0f1b5f702d 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -16,8 +16,6 @@ package com.android.settings.applications.manageapplications; -import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_SPA; - import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; @@ -67,7 +65,6 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; -import android.util.FeatureFlagUtils; import android.util.IconDrawableFactory; import android.util.Log; import android.view.LayoutInflater; @@ -728,9 +725,6 @@ public class ManageApplications extends InstrumentedFragment R.string.long_background_tasks_label); break; case LIST_TYPE_CLONED_APPS: - if (!FeatureFlagUtils.isEnabled(getContext(), SETTINGS_ENABLE_SPA)) { - return; - } int userId = UserHandle.getUserId(mCurrentUid); UserInfo userInfo = mUserManager.getUserInfo(userId); if (userInfo != null && !userInfo.isCloneProfile()) { From 212470d0a69c9cc659e03ae2e7f4270f08158ee0 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Thu, 11 May 2023 20:25:10 +0800 Subject: [PATCH 15/21] Move 'Audio Output' to Accessibility hearing device page * Extract the common part into HearingAidHelper. * Remove abstract getHearingDevice(). Change to get the hearing device when needed. * Move several classes from Bluetooth into Accessibility Bug: 281783079 Test: make RunSettingsRoboTests ROBOTEST_FILTER="(HearingDeviceAudioRoutingBasePreferenceControllerTest|AccessibilityHearingAidPreferenceControllerTest|HearingAidHelperTest|HearingAidAudioRoutingPreferenceControllerTest|HearingDeviceCallRoutingPreferenceControllerTest)" Change-Id: I79049107409b7086c6dcc8d48a6323e171ed1535 --- res/values/strings.xml | 4 +- ... accessibility_audio_routing_fragment.xml} | 8 +- res/xml/accessibility_hearing_aids.xml | 9 + res/xml/bluetooth_device_details_fragment.xml | 7 +- .../AccessibilityAudioRoutingFragment.java | 52 ++++++ ...ibilityHearingAidPreferenceController.java | 74 +------- .../AccessibilityHearingAidsFragment.java | 4 +- ...ngAidAudioRoutingPreferenceController.java | 38 ++++ .../accessibility/HearingAidHelper.java | 114 ++++++++++++ ...eAudioRoutingBasePreferenceController.java | 39 ++-- ...DeviceCallRoutingPreferenceController.java | 16 +- ...eviceMediaRoutingPreferenceController.java | 16 +- ...ceRingtoneRoutingPreferenceController.java | 16 +- ...stemSoundsRoutingPreferenceController.java | 16 +- ...luetoothDetailsAudioRoutingController.java | 86 --------- .../BluetoothDetailsAudioRoutingFragment.java | 87 --------- ...etailsHearingDeviceControlsController.java | 3 +- .../BluetoothDeviceDetailsFragment.java | 2 - ...ityHearingAidPreferenceControllerTest.java | 81 ++------- ...dAudioRoutingPreferenceControllerTest.java | 67 +++++++ .../accessibility/HearingAidHelperTest.java | 167 ++++++++++++++++++ ...ioRoutingBasePreferenceControllerTest.java | 65 ++++--- ...ceCallRoutingPreferenceControllerTest.java | 2 +- ...oothDetailsAudioRoutingControllerTest.java | 92 ---------- ...etoothDetailsAudioRoutingFragmentTest.java | 96 ---------- 25 files changed, 542 insertions(+), 619 deletions(-) rename res/xml/{bluetooth_audio_routing_fragment.xml => accessibility_audio_routing_fragment.xml} (85%) create mode 100644 src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java create mode 100644 src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceController.java create mode 100644 src/com/android/settings/accessibility/HearingAidHelper.java rename src/com/android/settings/{bluetooth => accessibility}/HearingDeviceAudioRoutingBasePreferenceController.java (79%) rename src/com/android/settings/{bluetooth => accessibility}/HearingDeviceCallRoutingPreferenceController.java (81%) rename src/com/android/settings/{bluetooth => accessibility}/HearingDeviceMediaRoutingPreferenceController.java (79%) rename src/com/android/settings/{bluetooth => accessibility}/HearingDeviceRingtoneRoutingPreferenceController.java (80%) rename src/com/android/settings/{bluetooth => accessibility}/HearingDeviceSystemSoundsRoutingPreferenceController.java (80%) delete mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingController.java delete mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragment.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java rename tests/robotests/src/com/android/settings/{bluetooth => accessibility}/HearingDeviceAudioRoutingBasePreferenceControllerTest.java (70%) rename tests/robotests/src/com/android/settings/{bluetooth => accessibility}/HearingDeviceCallRoutingPreferenceControllerTest.java (98%) delete mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingControllerTest.java delete mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragmentTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index dd9746610ae..fad70361b06 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -129,7 +129,9 @@ For all available hearing devices - Shortcuts & hearing aid compatibility + Hearing device settings + + Audio output, shortcut, hearing aid compatibility For this device diff --git a/res/xml/bluetooth_audio_routing_fragment.xml b/res/xml/accessibility_audio_routing_fragment.xml similarity index 85% rename from res/xml/bluetooth_audio_routing_fragment.xml rename to res/xml/accessibility_audio_routing_fragment.xml index 5edf6cdca41..12e75f22ab1 100644 --- a/res/xml/bluetooth_audio_routing_fragment.xml +++ b/res/xml/accessibility_audio_routing_fragment.xml @@ -31,7 +31,7 @@ android:key="audio_routing_ringtone" android:persistent="false" android:title="@string/bluetooth_ringtone_title" - settings:controller="com.android.settings.bluetooth.HearingDeviceRingtoneRoutingPreferenceController" /> + settings:controller="com.android.settings.accessibility.HearingDeviceRingtoneRoutingPreferenceController" /> + settings:controller="com.android.settings.accessibility.HearingDeviceCallRoutingPreferenceController" /> + settings:controller="com.android.settings.accessibility.HearingDeviceMediaRoutingPreferenceController" /> + settings:controller="com.android.settings.accessibility.HearingDeviceSystemSoundsRoutingPreferenceController" /> + + + - - + android:key="device_controls_general" /> diff --git a/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java b/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java new file mode 100644 index 00000000000..6eb2112d294 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; + +import android.app.settings.SettingsEnums; + +import com.android.settings.R; +import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; + +/** Settings fragment containing bluetooth audio routing. */ +public class AccessibilityAudioRoutingFragment extends RestrictedDashboardFragment { + private static final String TAG = "AccessibilityAudioRoutingFragment"; + + public AccessibilityAudioRoutingFragment() { + super(DISALLOW_CONFIG_BLUETOOTH); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.HEARING_AID_AUDIO_ROUTING; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_audio_routing_fragment; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.accessibility_audio_routing_fragment); +} diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java b/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java index cd76b47c22b..e4611fe97b3 100644 --- a/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java +++ b/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java @@ -17,7 +17,6 @@ package com.android.settings.accessibility; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; @@ -41,20 +40,14 @@ import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.HapClientProfile; import com.android.settingslib.bluetooth.HearingAidInfo; -import com.android.settingslib.bluetooth.HearingAidProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; -import java.util.ArrayList; -import java.util.List; import java.util.Set; -import java.util.stream.Collectors; /** * Controller that shows and updates the bluetooth device name @@ -73,10 +66,8 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC }; private final LocalBluetoothManager mLocalBluetoothManager; - private final BluetoothAdapter mBluetoothAdapter; private final LocalBluetoothProfileManager mProfileManager; - private final CachedBluetoothDeviceManager mCachedDeviceManager; - + private final HearingAidHelper mHelper; private FragmentManager mFragmentManager; public AccessibilityHearingAidPreferenceController(Context context, String preferenceKey) { @@ -84,8 +75,7 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC mLocalBluetoothManager = com.android.settings.bluetooth.Utils.getLocalBluetoothManager( context); mProfileManager = mLocalBluetoothManager.getProfileManager(); - mCachedDeviceManager = mLocalBluetoothManager.getCachedDeviceManager(); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mHelper = new HearingAidHelper(context); } @Override @@ -96,7 +86,7 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC @Override public int getAvailabilityStatus() { - return isHearingAidSupported() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return mHelper.isHearingAidSupported() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override @@ -111,7 +101,7 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC // Can't get connected hearing aids when hearing aids related profiles are not ready. The // profiles will be ready after the services are connected. Needs to add listener and // updates the information when all hearing aids related services are connected. - if (isAnyHearingAidRelatedProfilesNotReady()) { + if (!mHelper.isAllHearingAidRelatedProfilesReady()) { mProfileManager.addServiceListener(this); } } @@ -126,7 +116,7 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC @Override public boolean handlePreferenceTreeClick(Preference preference) { if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { - final CachedBluetoothDevice device = getConnectedHearingAidDevice(); + final CachedBluetoothDevice device = mHelper.getConnectedHearingAidDevice(); if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_ACCESSIBILITY_HEARING_AID_PAGE)) { launchHearingAidPage(); @@ -144,10 +134,7 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC @Override public CharSequence getSummary() { - if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { - return mContext.getText(R.string.accessibility_hearingaid_not_connected_summary); - } - final CachedBluetoothDevice device = getConnectedHearingAidDevice(); + final CachedBluetoothDevice device = mHelper.getConnectedHearingAidDevice(); if (device == null) { return mContext.getText(R.string.accessibility_hearingaid_not_connected_summary); } @@ -203,7 +190,7 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC @Override public void onServiceConnected() { - if (!isAnyHearingAidRelatedProfilesNotReady()) { + if (mHelper.isAllHearingAidRelatedProfilesReady()) { updateState(mHearingAidPreference); mProfileManager.removeServiceListener(this); } @@ -218,53 +205,8 @@ public class AccessibilityHearingAidPreferenceController extends BasePreferenceC mFragmentManager = fragmentManager; } - @VisibleForTesting - CachedBluetoothDevice getConnectedHearingAidDevice() { - final List deviceList = getConnectedHearingAidDeviceList(); - return deviceList.isEmpty() ? null : mCachedDeviceManager.findDevice(deviceList.get(0)); - } - private int getConnectedHearingAidDeviceNum() { - return getConnectedHearingAidDeviceList().size(); - } - - private List getConnectedHearingAidDeviceList() { - if (!isHearingAidSupported()) { - return new ArrayList<>(); - } - final List deviceList = new ArrayList<>(); - final HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile(); - if (hapClientProfile != null) { - deviceList.addAll(hapClientProfile.getConnectedDevices()); - } - final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); - if (hearingAidProfile != null) { - deviceList.addAll(hearingAidProfile.getConnectedDevices()); - } - return deviceList.stream() - .distinct() - .filter(d -> !mCachedDeviceManager.isSubDevice(d)).collect(Collectors.toList()); - } - - private boolean isHearingAidSupported() { - if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { - return false; - } - final List supportedList = mBluetoothAdapter.getSupportedProfiles(); - return supportedList.contains(BluetoothProfile.HEARING_AID) - || supportedList.contains(BluetoothProfile.HAP_CLIENT); - } - - private boolean isAnyHearingAidRelatedProfilesNotReady() { - HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); - if (hearingAidProfile != null && !hearingAidProfile.isProfileReady()) { - return true; - } - HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile(); - if (hapClientProfile != null && !hapClientProfile.isProfileReady()) { - return true; - } - return false; + return mHelper.getConnectedHearingAidDeviceList().size(); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java index 547c4568991..33fef62f205 100644 --- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java @@ -39,7 +39,7 @@ public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPrefe private static final String TAG = "AccessibilityHearingAidsFragment"; private static final String KEY_HEARING_OPTIONS_CATEGORY = "hearing_options_category"; - private static final int FIRST_PREFERENCE_IN_CATEGORY_INDEX = -1; + private static final int SHORTCUT_PREFERENCE_IN_CATEGORY_INDEX = 20; private String mFeatureName; public AccessibilityHearingAidsFragment() { @@ -65,7 +65,7 @@ public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPrefe final View view = super.onCreateView(inflater, container, savedInstanceState); final PreferenceCategory controlCategory = findPreference(KEY_HEARING_OPTIONS_CATEGORY); // To move the shortcut preference under controlCategory need to remove the original added. - mShortcutPreference.setOrder(FIRST_PREFERENCE_IN_CATEGORY_INDEX); + mShortcutPreference.setOrder(SHORTCUT_PREFERENCE_IN_CATEGORY_INDEX); getPreferenceScreen().removePreference(mShortcutPreference); controlCategory.addPreference(mShortcutPreference); return view; diff --git a/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceController.java b/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceController.java new file mode 100644 index 00000000000..54022ef7f9e --- /dev/null +++ b/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceController.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import android.content.Context; +import android.util.FeatureFlagUtils; + +import com.android.settings.core.BasePreferenceController; + +/** + * The controller of the audio routing. + */ +public class HearingAidAudioRoutingPreferenceController extends BasePreferenceController { + public HearingAidAudioRoutingPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING) + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/accessibility/HearingAidHelper.java b/src/com/android/settings/accessibility/HearingAidHelper.java new file mode 100644 index 00000000000..66a37f8dd58 --- /dev/null +++ b/src/com/android/settings/accessibility/HearingAidHelper.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; + +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A helper class to get and check hearing aids and its status. + */ +public class HearingAidHelper { + + private final BluetoothAdapter mBluetoothAdapter; + private final LocalBluetoothProfileManager mProfileManager; + private final CachedBluetoothDeviceManager mCachedDeviceManager; + + public HearingAidHelper(Context context) { + final LocalBluetoothManager localBluetoothManager = + com.android.settings.bluetooth.Utils.getLocalBluetoothManager(context); + mProfileManager = localBluetoothManager.getProfileManager(); + mCachedDeviceManager = localBluetoothManager.getCachedDeviceManager(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + } + + /** + * Gets the connected hearing aids device whose profiles are + * {@link BluetoothProfile#HEARING_AID} or {@link BluetoothProfile#HAP_CLIENT}. + * + * @return a list of hearing aids {@link BluetoothDevice} objects + */ + public List getConnectedHearingAidDeviceList() { + if (!isHearingAidSupported()) { + return new ArrayList<>(); + } + final List deviceList = new ArrayList<>(); + final HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile(); + if (hapClientProfile != null) { + deviceList.addAll(hapClientProfile.getConnectedDevices()); + } + final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + if (hearingAidProfile != null) { + deviceList.addAll(hearingAidProfile.getConnectedDevices()); + } + return deviceList.stream() + .distinct() + .filter(d -> !mCachedDeviceManager.isSubDevice(d)).collect(Collectors.toList()); + } + + /** + * Gets the first connected hearing aids device. + * + * @return a {@link CachedBluetoothDevice} that is hearing aids device + */ + public CachedBluetoothDevice getConnectedHearingAidDevice() { + final List deviceList = getConnectedHearingAidDeviceList(); + return deviceList.isEmpty() ? null : mCachedDeviceManager.findDevice(deviceList.get(0)); + } + + /** + * Checks if {@link BluetoothProfile#HEARING_AID} or {@link BluetoothProfile#HAP_CLIENT} + * supported. + */ + public boolean isHearingAidSupported() { + if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { + return false; + } + final List supportedList = mBluetoothAdapter.getSupportedProfiles(); + return supportedList.contains(BluetoothProfile.HEARING_AID) + || supportedList.contains(BluetoothProfile.HAP_CLIENT); + } + + /** + * Checks if {@link BluetoothProfile#HEARING_AID} or {@link BluetoothProfile#HAP_CLIENT} + * profiles all ready. + */ + public boolean isAllHearingAidRelatedProfilesReady() { + HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + if (hearingAidProfile != null && !hearingAidProfile.isProfileReady()) { + return false; + } + HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile(); + if (hapClientProfile != null && !hapClientProfile.isProfileReady()) { + return false; + } + return true; + } +} diff --git a/src/com/android/settings/bluetooth/HearingDeviceAudioRoutingBasePreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java similarity index 79% rename from src/com/android/settings/bluetooth/HearingDeviceAudioRoutingBasePreferenceController.java rename to src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java index e93863ae064..3599f4874c4 100644 --- a/src/com/android/settings/bluetooth/HearingDeviceAudioRoutingBasePreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; import android.content.ContentResolver; import android.content.Context; @@ -47,19 +47,24 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends private static final String TAG = "HARoutingBasePreferenceController"; private static final boolean DEBUG = false; - private final HearingAidAudioRoutingHelper mHelper; + private final HearingAidAudioRoutingHelper mAudioRoutingHelper; + private final HearingAidHelper mHearingAidHelper; public HearingDeviceAudioRoutingBasePreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - mHelper = new HearingAidAudioRoutingHelper(context); + this(context, preferenceKey, + new HearingAidAudioRoutingHelper(context), + new HearingAidHelper(context)); } @VisibleForTesting public HearingDeviceAudioRoutingBasePreferenceController(Context context, - String preferenceKey, HearingAidAudioRoutingHelper helper) { + String preferenceKey, HearingAidAudioRoutingHelper audioRoutingHelper, + HearingAidHelper hearingAidHelper) { super(context, preferenceKey); - mHelper = helper; + + mAudioRoutingHelper = audioRoutingHelper; + mHearingAidHelper = hearingAidHelper; } @Override @@ -81,7 +86,11 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends final Integer routingValue = Ints.tryParse((String) newValue); saveRoutingValue(mContext, routingValue); - trySetAudioRoutingConfig(getSupportedAttributeList(), getHearingDevice(), routingValue); + final CachedBluetoothDevice device = mHearingAidHelper.getConnectedHearingAidDevice(); + if (device != null) { + trySetAudioRoutingConfig(getSupportedAttributeList(), + mHearingAidHelper.getConnectedHearingAidDevice(), routingValue); + } return true; } @@ -89,10 +98,10 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends private void trySetAudioRoutingConfig(int[] audioAttributes, CachedBluetoothDevice hearingDevice, @HearingAidAudioRoutingConstants.RoutingValue int routingValue) { - final List supportedStrategies = mHelper.getSupportedStrategies( - audioAttributes); + final List supportedStrategies = + mAudioRoutingHelper.getSupportedStrategies(audioAttributes); final AudioDeviceAttributes hearingDeviceAttributes = - mHelper.getMatchedHearingDeviceAttributes(hearingDevice); + mAudioRoutingHelper.getMatchedHearingDeviceAttributes(hearingDevice); if (hearingDeviceAttributes == null) { if (DEBUG) { Log.d(TAG, @@ -103,8 +112,8 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends return; } - final boolean status = mHelper.setPreferredDeviceRoutingStrategies(supportedStrategies, - hearingDeviceAttributes, routingValue); + final boolean status = mAudioRoutingHelper.setPreferredDeviceRoutingStrategies( + supportedStrategies, hearingDeviceAttributes, routingValue); if (!status) { final List strategiesName = supportedStrategies.stream() @@ -121,12 +130,6 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends */ protected abstract int[] getSupportedAttributeList(); - /** - * Gets the {@link CachedBluetoothDevice} hearing device that is used to configure audio - * routing. - */ - protected abstract CachedBluetoothDevice getHearingDevice(); - /** * Saves the routing value. * diff --git a/src/com/android/settings/bluetooth/HearingDeviceCallRoutingPreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceCallRoutingPreferenceController.java similarity index 81% rename from src/com/android/settings/bluetooth/HearingDeviceCallRoutingPreferenceController.java rename to src/com/android/settings/accessibility/HearingDeviceCallRoutingPreferenceController.java index 4e5e1937b11..8ca266359c2 100644 --- a/src/com/android/settings/bluetooth/HearingDeviceCallRoutingPreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceCallRoutingPreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; import android.content.Context; import android.provider.Settings; @@ -35,15 +35,6 @@ public class HearingDeviceCallRoutingPreferenceController extends super(context, preferenceKey); } - /** - * Initializes objects in this controller. Need to call this before using the controller. - * - * @param cachedBluetoothDevice the hearing device to configure audio routing - */ - public void init(CachedBluetoothDevice cachedBluetoothDevice) { - mHearingDevice = cachedBluetoothDevice; - } - @Override public int getAvailabilityStatus() { return Utils.isVoiceCapable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; @@ -54,11 +45,6 @@ public class HearingDeviceCallRoutingPreferenceController extends return HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES; } - @Override - protected CachedBluetoothDevice getHearingDevice() { - return mHearingDevice; - } - @Override protected void saveRoutingValue(Context context, int routingValue) { Settings.Secure.putInt(context.getContentResolver(), diff --git a/src/com/android/settings/bluetooth/HearingDeviceMediaRoutingPreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceMediaRoutingPreferenceController.java similarity index 79% rename from src/com/android/settings/bluetooth/HearingDeviceMediaRoutingPreferenceController.java rename to src/com/android/settings/accessibility/HearingDeviceMediaRoutingPreferenceController.java index 1ea36c783e2..cd99ce0c0d0 100644 --- a/src/com/android/settings/bluetooth/HearingDeviceMediaRoutingPreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceMediaRoutingPreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; import android.content.Context; import android.provider.Settings; @@ -34,26 +34,12 @@ public class HearingDeviceMediaRoutingPreferenceController extends super(context, preferenceKey); } - /** - * Initializes objects in this controller. Need to call this before using the controller. - * - * @param cachedBluetoothDevice the hearing device to configure audio routing - */ - public void init(CachedBluetoothDevice cachedBluetoothDevice) { - mHearingDevice = cachedBluetoothDevice; - } - @Override protected int[] getSupportedAttributeList() { return HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES; } - @Override - protected CachedBluetoothDevice getHearingDevice() { - return mHearingDevice; - } - @Override protected void saveRoutingValue(Context context, int routingValue) { Settings.Secure.putInt(context.getContentResolver(), diff --git a/src/com/android/settings/bluetooth/HearingDeviceRingtoneRoutingPreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceRingtoneRoutingPreferenceController.java similarity index 80% rename from src/com/android/settings/bluetooth/HearingDeviceRingtoneRoutingPreferenceController.java rename to src/com/android/settings/accessibility/HearingDeviceRingtoneRoutingPreferenceController.java index 006cb6b217a..3e8f79cec29 100644 --- a/src/com/android/settings/bluetooth/HearingDeviceRingtoneRoutingPreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceRingtoneRoutingPreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; import android.content.Context; import android.provider.Settings; @@ -34,26 +34,12 @@ public class HearingDeviceRingtoneRoutingPreferenceController extends super(context, preferenceKey); } - /** - * Initializes objects in this controller. Need to call this before using the controller. - * - * @param cachedBluetoothDevice the hearing device to configure audio routing - */ - public void init(CachedBluetoothDevice cachedBluetoothDevice) { - mHearingDevice = cachedBluetoothDevice; - } - @Override protected int[] getSupportedAttributeList() { return HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE; } - @Override - protected CachedBluetoothDevice getHearingDevice() { - return mHearingDevice; - } - @Override protected void saveRoutingValue(Context context, int routingValue) { Settings.Secure.putInt(context.getContentResolver(), diff --git a/src/com/android/settings/bluetooth/HearingDeviceSystemSoundsRoutingPreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceSystemSoundsRoutingPreferenceController.java similarity index 80% rename from src/com/android/settings/bluetooth/HearingDeviceSystemSoundsRoutingPreferenceController.java rename to src/com/android/settings/accessibility/HearingDeviceSystemSoundsRoutingPreferenceController.java index a66cfab93a0..1bc0090c7e9 100644 --- a/src/com/android/settings/bluetooth/HearingDeviceSystemSoundsRoutingPreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceSystemSoundsRoutingPreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; import android.content.Context; import android.provider.Settings; @@ -35,26 +35,12 @@ public class HearingDeviceSystemSoundsRoutingPreferenceController extends super(context, preferenceKey); } - /** - * Initializes objects in this controller. Need to call this before using the controller. - * - * @param cachedBluetoothDevice the hearing device to configure audio routing - */ - public void init(CachedBluetoothDevice cachedBluetoothDevice) { - mHearingDevice = cachedBluetoothDevice; - } - @Override protected int[] getSupportedAttributeList() { return HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES; } - @Override - protected CachedBluetoothDevice getHearingDevice() { - return mHearingDevice; - } - @Override protected void saveRoutingValue(Context context, int routingValue) { Settings.Secure.putInt(context.getContentResolver(), diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingController.java deleted file mode 100644 index 91221a33ba9..00000000000 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingController.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth; - -import static com.android.settings.bluetooth.BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS; - -import android.content.Context; -import android.os.Bundle; -import android.util.FeatureFlagUtils; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceScreen; - -import com.android.settings.R; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.core.lifecycle.Lifecycle; - -/** - * The controller of the audio routing in the bluetooth detail settings. - */ -public class BluetoothDetailsAudioRoutingController extends BluetoothDetailsController { - - private static final String KEY_DEVICE_CONTROLS_SPECIFIC_GROUP = "device_controls_specific"; - @VisibleForTesting - static final String KEY_AUDIO_ROUTING = "audio_routing"; - - public BluetoothDetailsAudioRoutingController(Context context, - PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { - super(context, fragment, device, lifecycle); - } - - @Override - public boolean isAvailable() { - return mCachedDevice.isHearingAidDevice() && FeatureFlagUtils.isEnabled(mContext, - FeatureFlagUtils.SETTINGS_AUDIO_ROUTING); - } - - @Override - protected void init(PreferenceScreen screen) { - if (!mCachedDevice.isHearingAidDevice()) { - return; - } - - final PreferenceCategory prefCategory = screen.findPreference(getPreferenceKey()); - final Preference pref = createAudioRoutingPreference(prefCategory.getContext()); - prefCategory.addPreference(pref); - } - - @Override - protected void refresh() {} - - @Override - public String getPreferenceKey() { - return KEY_DEVICE_CONTROLS_SPECIFIC_GROUP; - } - - private Preference createAudioRoutingPreference(Context context) { - final Preference preference = new Preference(context); - - preference.setKey(KEY_AUDIO_ROUTING); - preference.setTitle(context.getString(R.string.bluetooth_audio_routing_title)); - preference.setSummary(context.getString(R.string.bluetooth_audio_routing_summary)); - final Bundle extras = preference.getExtras(); - extras.putString(KEY_DEVICE_ADDRESS, mCachedDevice.getAddress()); - preference.setFragment(BluetoothDetailsAudioRoutingFragment.class.getName()); - - return preference; - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragment.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragment.java deleted file mode 100644 index 6c435a27cd2..00000000000 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth; - -import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; - -import static com.android.settings.bluetooth.BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS; - -import android.app.settings.SettingsEnums; -import android.bluetooth.BluetoothDevice; -import android.content.Context; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; - -import com.android.settings.R; -import com.android.settings.dashboard.RestrictedDashboardFragment; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -/** Settings fragment containing bluetooth audio routing. */ -public class BluetoothDetailsAudioRoutingFragment extends RestrictedDashboardFragment { - - public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider(R.xml.bluetooth_audio_routing_fragment); - private static final String TAG = "BluetoothDetailsAudioRoutingFragment"; - @VisibleForTesting - CachedBluetoothDevice mCachedDevice; - - public BluetoothDetailsAudioRoutingFragment() { - super(DISALLOW_CONFIG_BLUETOOTH); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - final LocalBluetoothManager localBtMgr = Utils.getLocalBtManager(context); - final CachedBluetoothDeviceManager cachedDeviceMgr = localBtMgr.getCachedDeviceManager(); - final BluetoothDevice bluetoothDevice = localBtMgr.getBluetoothAdapter().getRemoteDevice( - getArguments().getString(KEY_DEVICE_ADDRESS)); - - mCachedDevice = cachedDeviceMgr.findDevice(bluetoothDevice); - if (mCachedDevice == null) { - // Close this page if device is null with invalid device mac address - Log.w(TAG, "onAttach() CachedDevice is null! Can not find address: " - + bluetoothDevice.getAnonymizedAddress()); - finish(); - return; - } - - use(HearingDeviceRingtoneRoutingPreferenceController.class).init(mCachedDevice); - use(HearingDeviceCallRoutingPreferenceController.class).init(mCachedDevice); - use(HearingDeviceMediaRoutingPreferenceController.class).init(mCachedDevice); - use(HearingDeviceSystemSoundsRoutingPreferenceController.class).init(mCachedDevice); - } - - @Override - public int getMetricsCategory() { - return SettingsEnums.BLUETOOTH_AUDIO_ROUTING; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.bluetooth_audio_routing_fragment; - } - - @Override - protected String getLogTag() { - return TAG; - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java index a3b1105015d..c4a422186dd 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java @@ -34,7 +34,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import com.google.common.annotations.VisibleForTesting; /** - * The controller of the hearing device controls in the bluetooth detail settings. + * The controller of the hearing device settings to launch Hearing device page. */ public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDetailsController implements Preference.OnPreferenceClickListener { @@ -87,6 +87,7 @@ public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDe final Preference preference = new Preference(context); preference.setKey(KEY_HEARING_DEVICE_CONTROLS); preference.setTitle(context.getString(R.string.bluetooth_device_controls_title)); + preference.setSummary(context.getString(R.string.bluetooth_device_controls_summary)); preference.setOnPreferenceClickListener(this); return preference; diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 2a94b1e077e..99f3e3187cb 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -314,8 +314,6 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle)); controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this, mCachedDevice, lifecycle)); - controllers.add(new BluetoothDetailsAudioRoutingController(context, this, mCachedDevice, - lifecycle)); } return controllers; } diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceControllerTest.java index 5ee7ab3d13c..c68e90bdc17 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceControllerTest.java @@ -24,25 +24,22 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.FragmentActivity; import androidx.preference.Preference; +import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.bluetooth.Utils; -import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; -import com.android.settings.utils.ActivityControllerWrapper; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; @@ -55,11 +52,12 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import org.junit.After; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.Robolectric; +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; @@ -74,6 +72,9 @@ import java.util.Set; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) public class AccessibilityHearingAidPreferenceControllerTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; private static final String TEST_DEVICE_ADDRESS_2 = "00:A2:A2:A2:A2:A2"; private static final String TEST_DEVICE_NAME = "TEST_HEARING_AID_BT_DEVICE_NAME"; @@ -82,7 +83,8 @@ public class AccessibilityHearingAidPreferenceControllerTest { private BluetoothAdapter mBluetoothAdapter; private ShadowBluetoothAdapter mShadowBluetoothAdapter; private BluetoothDevice mBluetoothDevice; - private Activity mContext; + private final Context mContext = ApplicationProvider.getApplicationContext(); + private Preference mHearingAidPreference; private AccessibilityHearingAidPreferenceController mPreferenceController; private ShadowApplication mShadowApplication; @@ -106,11 +108,7 @@ public class AccessibilityHearingAidPreferenceControllerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); mShadowApplication = ShadowApplication.getInstance(); - - mContext = spy((Activity) ActivityControllerWrapper.setup( - Robolectric.buildActivity(Activity.class)).get()); setupEnvironment(); mHearingAidPreference = new Preference(mContext); @@ -274,65 +272,6 @@ public class AccessibilityHearingAidPreferenceControllerTest { verify(mPreferenceController).launchBluetoothDeviceDetailSetting(mCachedBluetoothDevice); } - @Test - public void onSupportHearingAidProfile_isAvailable() { - mShadowBluetoothAdapter.clearSupportedProfiles(); - mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); - mPreferenceController = new AccessibilityHearingAidPreferenceController(mContext, - HEARING_AID_PREFERENCE); - mPreferenceController.setPreference(mHearingAidPreference); - - assertThat(mPreferenceController.isAvailable()).isTrue(); - } - - @Test - public void onSupportHapClientProfile_isAvailable() { - mShadowBluetoothAdapter.clearSupportedProfiles(); - mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HAP_CLIENT); - mPreferenceController = new AccessibilityHearingAidPreferenceController(mContext, - HEARING_AID_PREFERENCE); - mPreferenceController.setPreference(mHearingAidPreference); - - assertThat(mPreferenceController.isAvailable()).isTrue(); - } - - @Test - public void onNotSupportAnyHearingAidRelatedProfile_isNotAvailable() { - mShadowBluetoothAdapter.clearSupportedProfiles(); - mPreferenceController = new AccessibilityHearingAidPreferenceController(mContext, - HEARING_AID_PREFERENCE); - mPreferenceController.setPreference(mHearingAidPreference); - - assertThat(mPreferenceController.isAvailable()).isFalse(); - } - - @Test - public void getConnectedHearingAidDevice_doNotReturnSubDevice() { - when(mHearingAidProfile.getConnectedDevices()).thenReturn(generateHearingAidDeviceList()); - when(mLocalBluetoothManager.getCachedDeviceManager().isSubDevice(mBluetoothDevice)) - .thenReturn(true); - - assertThat(mPreferenceController.getConnectedHearingAidDevice()).isNull(); - } - - @Test - @Config(shadows = ShadowAlertDialogCompat.class) - public void onActiveDeviceChanged_hearingAidProfile_launchHearingAidPairingDialog() { - final FragmentActivity mActivity = Robolectric.setupActivity(FragmentActivity.class); - when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true); - when(mCachedBluetoothDevice.getDeviceMode()).thenReturn( - HearingAidInfo.DeviceMode.MODE_BINAURAL); - when(mCachedBluetoothDevice.getDeviceSide()).thenReturn( - HearingAidInfo.DeviceSide.SIDE_LEFT); - mPreferenceController.setFragmentManager(mActivity.getSupportFragmentManager()); - - mPreferenceController.onActiveDeviceChanged(mCachedBluetoothDevice, - BluetoothProfile.HEARING_AID); - - final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - assertThat(dialog.isShowing()).isTrue(); - } - @Test public void onServiceConnected_onHearingAidProfileConnected_updateSummary() { when(mCachedBluetoothDevice.getDeviceSide()).thenReturn( diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceControllerTest.java new file mode 100644 index 00000000000..d16bc4351c3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HearingAidAudioRoutingPreferenceControllerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.util.FeatureFlagUtils; + +import androidx.test.core.app.ApplicationProvider; + +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; + +/** Tests for {@link HearingAidAudioRoutingPreferenceController}. */ +@RunWith(RobolectricTestRunner.class) +public class HearingAidAudioRoutingPreferenceControllerTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + + private HearingAidAudioRoutingPreferenceController mController; + + @Before + public void setUp() { + mController = new HearingAidAudioRoutingPreferenceController(mContext, "test_key"); + } + + @Test + public void getAvailabilityStatus_audioRoutingNotSupported_returnUnsupported() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_audioRoutingNotSupported_available() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java new file mode 100644 index 00000000000..194b766dd75 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HearingAidHelperTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +import java.util.ArrayList; +import java.util.Collections; + +/** Tests for {@link HearingAidHelper}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) +public class HearingAidHelperTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; + + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + @Mock + private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + @Mock + private CachedBluetoothDeviceManager mCachedDeviceManager; + @Mock + private CachedBluetoothDevice mCachedBluetoothDevice; + @Mock + private HearingAidProfile mHearingAidProfile; + @Mock + private HapClientProfile mHapClientProfile; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private BluetoothAdapter mBluetoothAdapter; + private BluetoothDevice mBluetoothDevice; + private HearingAidHelper mHelper; + + @Before + public void setUp() { + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mShadowBluetoothAdapter = Shadow.extract(mBluetoothAdapter); + mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS); + when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); + when(mCachedDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + when(mLocalBluetoothProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); + + mHelper = new HearingAidHelper(mContext); + } + + @Test + public void isHearingAidSupported_supported_returnTrue() { + mBluetoothAdapter.enable(); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + + assertThat(mHelper.isHearingAidSupported()).isTrue(); + } + + @Test + public void isHearingAidSupported_bluetoothOff_returnFalse() { + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + mBluetoothAdapter.disable(); + + assertThat(mHelper.isHearingAidSupported()).isFalse(); + } + + + @Test + public void isAllHearingAidRelatedProfilesReady_allReady_returnTrue() { + when(mHearingAidProfile.isProfileReady()).thenReturn(true); + when(mHapClientProfile.isProfileReady()).thenReturn(true); + + assertThat(mHelper.isAllHearingAidRelatedProfilesReady()).isTrue(); + } + + @Test + public void isAllHearingAidRelatedProfilesReady_notFullReady_returnFalse() { + when(mHearingAidProfile.isProfileReady()).thenReturn(false); + when(mHapClientProfile.isProfileReady()).thenReturn(true); + + assertThat(mHelper.isAllHearingAidRelatedProfilesReady()).isFalse(); + } + + @Test + public void getConnectedHearingAidDeviceList_oneDeviceAdded_getOneDevice() { + mBluetoothAdapter.enable(); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + when(mHearingAidProfile.getConnectedDevices()).thenReturn(new ArrayList<>( + Collections.singletonList(mBluetoothDevice))); + + assertThat(mHelper.getConnectedHearingAidDeviceList().size()).isEqualTo(1); + } + + @Test + public void getConnectedHearingAidDeviceList_oneSubDeviceAdded_getZeroDevice() { + mBluetoothAdapter.enable(); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + when(mHearingAidProfile.getConnectedDevices()).thenReturn(new ArrayList<>( + Collections.singletonList(mBluetoothDevice))); + when(mLocalBluetoothManager.getCachedDeviceManager().isSubDevice( + mBluetoothDevice)).thenReturn(true); + + assertThat(mHelper.getConnectedHearingAidDeviceList().size()).isEqualTo(0); + } + + @Test + public void getConnectedHearingAidDevice_getExpectedCachedBluetoothDevice() { + mBluetoothAdapter.enable(); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + when(mHearingAidProfile.getConnectedDevices()).thenReturn(new ArrayList<>( + Collections.singletonList(mBluetoothDevice))); + + assertThat(mHelper.getConnectedHearingAidDevice()).isEqualTo(mCachedBluetoothDevice); + assertThat(mCachedBluetoothDevice.getAddress()).isEqualTo(mBluetoothDevice.getAddress()); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceAudioRoutingBasePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java similarity index 70% rename from tests/robotests/src/com/android/settings/bluetooth/HearingDeviceAudioRoutingBasePreferenceControllerTest.java rename to tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java index 105da6546ad..4decf68d68c 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceAudioRoutingBasePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; 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.isNull; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,9 +38,15 @@ import androidx.preference.ListPreference; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants; import com.android.settingslib.bluetooth.HearingAidAudioRoutingHelper; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import org.junit.Before; import org.junit.Rule; @@ -50,11 +57,13 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.List; /** Tests for {@link HearingDeviceAudioRoutingBasePreferenceController}. */ @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) public class HearingDeviceAudioRoutingBasePreferenceControllerTest { @Rule @@ -63,8 +72,14 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { @Spy private final Context mContext = ApplicationProvider.getApplicationContext(); private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; - private static final String FAKE_KEY = "fake_key"; + private final ListPreference mListPreference = new ListPreference(mContext); + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + @Mock + private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + @Mock + private CachedBluetoothDeviceManager mCachedDeviceManager; @Mock private AudioProductStrategy mAudioProductStrategyMedia; @Mock @@ -72,8 +87,10 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { @Mock private BluetoothDevice mBluetoothDevice; @Spy - private HearingAidAudioRoutingHelper mHelper = new HearingAidAudioRoutingHelper(mContext); - private final ListPreference mListPreference = new ListPreference(mContext); + private HearingAidAudioRoutingHelper mAudioRoutingHelper = + new HearingAidAudioRoutingHelper(mContext); + @Mock + private HearingAidHelper mHearingAidHelper; private TestHearingDeviceAudioRoutingBasePreferenceController mController; @Before @@ -83,19 +100,23 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { AudioDeviceInfo.TYPE_HEARING_AID, TEST_DEVICE_ADDRESS); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); when(mBluetoothDevice.getAnonymizedAddress()).thenReturn(TEST_DEVICE_ADDRESS); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); - when(mHelper.getMatchedHearingDeviceAttributes(any())).thenReturn(hearingDeviceAttribute); + doReturn(hearingDeviceAttribute).when( + mAudioRoutingHelper).getMatchedHearingDeviceAttributes(any()); when(mAudioProductStrategyMedia.getAudioAttributesForLegacyStreamType( - AudioManager.STREAM_MUSIC)) - .thenReturn((new AudioAttributes.Builder()).build()); - when(mHelper.getAudioProductStrategies()).thenReturn(List.of(mAudioProductStrategyMedia)); + AudioManager.STREAM_MUSIC)).thenReturn((new AudioAttributes.Builder()).build()); + when(mAudioRoutingHelper.getAudioProductStrategies()).thenReturn( + List.of(mAudioProductStrategyMedia)); - mController = new TestHearingDeviceAudioRoutingBasePreferenceController(mContext, FAKE_KEY, - mHelper); - TestHearingDeviceAudioRoutingBasePreferenceController.setupForTesting( - mCachedBluetoothDevice); + mController = new TestHearingDeviceAudioRoutingBasePreferenceController(mContext, + "test_key", + mAudioRoutingHelper, mHearingAidHelper); mListPreference.setEntries(R.array.bluetooth_audio_routing_titles); mListPreference.setEntryValues(R.array.bluetooth_audio_routing_values); mListPreference.setSummary("%s"); @@ -122,20 +143,21 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { @Test public void onPreferenceChange_noMatchedDeviceAttributes_notCallSetStrategies() { - when(mHelper.getMatchedHearingDeviceAttributes(any())).thenReturn(null); + when(mAudioRoutingHelper.getMatchedHearingDeviceAttributes(any())).thenReturn(null); - verify(mHelper, never()).setPreferredDeviceRoutingStrategies(any(), isNull(), anyInt()); + verify(mAudioRoutingHelper, never()).setPreferredDeviceRoutingStrategies(any(), isNull(), + anyInt()); } private static class TestHearingDeviceAudioRoutingBasePreferenceController extends HearingDeviceAudioRoutingBasePreferenceController { - private static CachedBluetoothDevice sCachedBluetoothDevice; private static int sSavedRoutingValue; TestHearingDeviceAudioRoutingBasePreferenceController(Context context, - String preferenceKey, HearingAidAudioRoutingHelper helper) { - super(context, preferenceKey, helper); + String preferenceKey, HearingAidAudioRoutingHelper audioRoutingHelper, + HearingAidHelper hearingAidHelper) { + super(context, preferenceKey, audioRoutingHelper, hearingAidHelper); } @Override @@ -143,11 +165,6 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { return new int[]{AudioAttributes.USAGE_MEDIA}; } - @Override - protected CachedBluetoothDevice getHearingDevice() { - return sCachedBluetoothDevice; - } - @Override protected void saveRoutingValue(Context context, int routingValue) { sSavedRoutingValue = routingValue; @@ -157,9 +174,5 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { protected int restoreRoutingValue(Context context) { return sSavedRoutingValue; } - - public static void setupForTesting(CachedBluetoothDevice cachedBluetoothDevice) { - sCachedBluetoothDevice = cachedBluetoothDevice; - } } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceCallRoutingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceCallRoutingPreferenceControllerTest.java similarity index 98% rename from tests/robotests/src/com/android/settings/bluetooth/HearingDeviceCallRoutingPreferenceControllerTest.java rename to tests/robotests/src/com/android/settings/accessibility/HearingDeviceCallRoutingPreferenceControllerTest.java index dec4cc4bb01..8eed2940984 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceCallRoutingPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceCallRoutingPreferenceControllerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth; +package com.android.settings.accessibility; import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingControllerTest.java deleted file mode 100644 index ea65856de61..00000000000 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingControllerTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth; - -import static com.android.settings.bluetooth.BluetoothDetailsAudioRoutingController.KEY_AUDIO_ROUTING; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.when; - -import android.util.FeatureFlagUtils; - -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; - -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; - -/** Tests for {@link BluetoothDetailsAudioRoutingController}. */ -@RunWith(RobolectricTestRunner.class) -public class BluetoothDetailsAudioRoutingControllerTest extends - BluetoothDetailsControllerTestBase { - @Rule - public final MockitoRule mockito = MockitoJUnit.rule(); - - private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; - - private BluetoothDetailsAudioRoutingController mController; - - @Override - public void setUp() { - super.setUp(); - - mController = new BluetoothDetailsAudioRoutingController(mContext, mFragment, mCachedDevice, - mLifecycle); - final PreferenceCategory preferenceCategory = new PreferenceCategory(mContext); - preferenceCategory.setKey(mController.getPreferenceKey()); - mScreen.addPreference(preferenceCategory); - } - - @Test - public void isAvailable_isHearingAidDevice_available() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); - when(mCachedDevice.isHearingAidDevice()).thenReturn(true); - - assertThat(mController.isAvailable()).isTrue(); - } - - @Test - public void isAvailable_isNotHearingAidDevice_notAvailable() { - FeatureFlagUtils.setEnabled(mContext, - FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); - when(mCachedDevice.isHearingAidDevice()).thenReturn(false); - - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void init_isHearingAidDevice_expectedAudioRoutingPreference() { - when(mCachedDevice.isHearingAidDevice()).thenReturn(true); - when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); - - mController.init(mScreen); - final Preference preference = mScreen.findPreference(KEY_AUDIO_ROUTING); - final String address = preference.getExtras().getString( - BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS); - final String fragment = preference.getFragment(); - - assertThat(address).isEqualTo(TEST_ADDRESS); - assertThat(fragment).isEqualTo(BluetoothDetailsAudioRoutingFragment.class.getName()); - - } -} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragmentTest.java deleted file mode 100644 index 9bd4f1b8d84..00000000000 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioRoutingFragmentTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.when; - -import android.bluetooth.BluetoothDevice; -import android.content.Context; -import android.os.Bundle; - -import androidx.test.core.app.ApplicationProvider; - -import com.android.settings.testutils.shadow.ShadowBluetoothUtils; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** Tests for {@link BluetoothDetailsAudioRoutingFragment}. */ -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowBluetoothUtils.class}) -public class BluetoothDetailsAudioRoutingFragmentTest { - - @Rule - public MockitoRule mMockitoRule = MockitoJUnit.rule(); - - private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; - - private final Context mContext = ApplicationProvider.getApplicationContext(); - - private BluetoothDetailsAudioRoutingFragment mFragment; - @Mock - private LocalBluetoothManager mLocalBluetoothManager; - @Mock - private CachedBluetoothDeviceManager mCachedDeviceManager; - @Mock - private LocalBluetoothAdapter mLocalBluetoothAdapter; - @Mock - private BluetoothDevice mBluetoothDevice; - @Mock - private CachedBluetoothDevice mCachedDevice; - - @Before - public void setUp() { - setupEnvironment(); - - when(mLocalBluetoothAdapter.getRemoteDevice(TEST_ADDRESS)).thenReturn(mBluetoothDevice); - when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); - when(mCachedDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedDevice); - - mFragment = new BluetoothDetailsAudioRoutingFragment(); - } - - @Test - public void onAttach_setArgumentsWithAddress_expectedCachedDeviceWithAddress() { - final Bundle args = new Bundle(); - args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, TEST_ADDRESS); - mFragment.setArguments(args); - - mFragment.onAttach(mContext); - - assertThat(mFragment.mCachedDevice.getAddress()).isEqualTo(TEST_ADDRESS); - } - - private void setupEnvironment() { - ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; - when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); - when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter); - } -} From 0faf38eadd1a124e7d91e46d8c1c5f3df78692ca Mon Sep 17 00:00:00 2001 From: SongFerngWang Date: Fri, 12 May 2023 17:18:37 +0800 Subject: [PATCH 16/21] The UI does not remove the preference The LE audio structure have two or more devices, it use CSIP to combine them with the groupId. It breaks the UI structure, since the UI use the map to save relationship between the bluetoothDevice and preference. There are two or more devices using the same UI, it causes UI show the wrong preference when CSIP do switching of device. Remove the unuse device when UI refreshing. Bug: 281697186 Test: Build pass Test: make RunSettingsRoboTests ROBOTEST_FILTER=AvailableMediaDeviceGroupControllerTest Change-Id: I798cf9edb590c4a25273913d2f2faf0ed4364ba9 --- .../bluetooth/BluetoothDeviceUpdater.java | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java index 17c5f36d5c6..2935c6779dc 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java @@ -35,8 +35,10 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -125,7 +127,7 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, update(cachedBluetoothDevice); } } else { - removeAllDevicesFromPreference(); + removeAllDevicesFromPreference(); } } @@ -252,7 +254,7 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, btPreference.setOnGearClickListener(mDeviceProfilesListener); if (this instanceof Preference.OnPreferenceClickListener) { btPreference.setOnPreferenceClickListener( - (Preference.OnPreferenceClickListener)this); + (Preference.OnPreferenceClickListener) this); } mPreferenceMap.put(device, btPreference); mDevicePreferenceCallback.onDeviceAdded(btPreference); @@ -266,17 +268,20 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, final BluetoothDevice device = cachedDevice.getDevice(); final CachedBluetoothDevice subCachedDevice = cachedDevice.getSubDevice(); if (mPreferenceMap.containsKey(device)) { - mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(device)); - mPreferenceMap.remove(device); + removePreference(device); } else if (subCachedDevice != null) { // When doing remove, to check if preference maps to sub device. // This would happen when connection state is changed in detail page that there is no // callback from SettingsLib. final BluetoothDevice subDevice = subCachedDevice.getDevice(); - if (mPreferenceMap.containsKey(subDevice)) { - mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(subDevice)); - mPreferenceMap.remove(subDevice); - } + removePreference(subDevice); + } + } + + private void removePreference(BluetoothDevice device) { + if (mPreferenceMap.containsKey(device)) { + mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(device)); + mPreferenceMap.remove(device); } } @@ -324,14 +329,38 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, * Update the attributes of {@link Preference}. */ public void refreshPreference() { - for (Preference preference : mPreferenceMap.values()) { - ((BluetoothDevicePreference) preference).onPreferenceAttributesChanged(); + List removeList = new ArrayList<>(); + mPreferenceMap.forEach((key, preference) -> { + if (isDeviceOfMapInCachedDevicesList(key)) { + ((BluetoothDevicePreference) preference).onPreferenceAttributesChanged(); + } else { + // If the BluetoothDevice of preference is not in the CachedDevices List, then + // remove this preference. + removeList.add(key); + } + }); + + for (BluetoothDevice bluetoothDevice : removeList) { + Log.d(getLogTag(), "removePreference key: " + bluetoothDevice.getAnonymizedAddress()); + removePreference(bluetoothDevice); } } - protected boolean isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice){ + protected boolean isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice) { return mLocalManager.getCachedDeviceManager().getCachedDevicesCopy().contains(cachedDevice); } + + private boolean isDeviceOfMapInCachedDevicesList(BluetoothDevice inputBluetoothDevice) { + Collection cachedDevices = + mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); + if (cachedDevices == null || cachedDevices.isEmpty()) { + return false; + } + return cachedDevices.stream() + .anyMatch(cachedBluetoothDevice -> cachedBluetoothDevice.getDevice() != null + && cachedBluetoothDevice.getDevice().equals(inputBluetoothDevice)); + } + protected String getLogTag() { return TAG; } From 3988d11dddec8126c60b4e5a12950915bdc83647 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 12 May 2023 16:25:27 +0800 Subject: [PATCH 17/21] Fix Bluetooth tethering toggle status The mBluetoothPan is updated async, toggle status is set before data loaded. Refresh the status after load to fix this issue. Fix: 272525310 Test: Manually when Bluetooth tethering is on Change-Id: Ia306f3648ba8d32a63a8ec6c72260f7f43b830bc --- src/com/android/settings/network/tether/TetherSettings.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/network/tether/TetherSettings.java b/src/com/android/settings/network/tether/TetherSettings.java index ba19d1c129b..9fa8730cf10 100644 --- a/src/com/android/settings/network/tether/TetherSettings.java +++ b/src/com/android/settings/network/tether/TetherSettings.java @@ -171,6 +171,8 @@ public class TetherSettings extends RestrictedSettingsFragment return; } + setupTetherPreference(); + final Activity activity = getActivity(); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter != null) { @@ -184,7 +186,6 @@ public class TetherSettings extends RestrictedSettingsFragment new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); } - setupTetherPreference(); setTopIntroPreferenceTitle(); mDataSaverBackend.addListener(this); @@ -605,6 +606,7 @@ public class TetherSettings extends RestrictedSettingsFragment public void onServiceConnected(int profile, BluetoothProfile proxy) { if (mBluetoothPan.get() == null) { mBluetoothPan.set((BluetoothPan) proxy); + updateBluetoothState(); } } From 77deb463a0bed6f5cf784d16acb35767028c6b5c Mon Sep 17 00:00:00 2001 From: Wesley Wang Date: Fri, 12 May 2023 17:40:05 +0800 Subject: [PATCH 18/21] Update battery settings remaining time format - Update the remaining time format to align with status bar - Remove unused test case since no more less than 7 or 15 remaining time case for battery settings screenshots: https://screenshot.googleplex.com/8jPYPj7yznGFU4b.png https://screenshot.googleplex.com/5GYXA2tusSUVmVQ.png https://screenshot.googleplex.com/3EnB3ejAxAzP28q.png https://screenshot.googleplex.com/5jeUxwBEyHotf9d.png Bug: 281685505 Test: make SettingsLibRoboTests Change-Id: If33e1828582845f78d8ef666c2b74ab5bba22357 --- .../settings/fuelgauge/BatteryInfo.java | 13 ++---- .../settings/fuelgauge/BatteryInfoTest.java | 40 ------------------- 2 files changed, 3 insertions(+), 50 deletions(-) diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index 98240447aeb..300db23007c 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -322,18 +322,11 @@ public class BatteryInfo { final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis()); if (drainTimeUs > 0) { info.remainingTimeUs = drainTimeUs; - info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted( + info.remainingLabel = PowerUtil.getBatteryRemainingShortStringFormatted( context, - PowerUtil.convertUsToMs(drainTimeUs), - null /* percentageString */, - false /* basedOnUsage */ - ); - info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted( - context, - PowerUtil.convertUsToMs(drainTimeUs), - info.batteryPercentString, - estimate.isBasedOnUsage() && !shortString + PowerUtil.convertUsToMs(drainTimeUs) ); + info.chargeLabel = info.remainingLabel; info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted( context, PowerUtil.convertUsToMs(drainTimeUs)); } else { diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java index eb4b598823f..ded108cfc89 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java @@ -43,7 +43,6 @@ import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.settings.testutils.BatteryTestUtils; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.widget.UsageView; -import com.android.settingslib.R; import com.android.settingslib.fuelgauge.Estimate; import org.junit.Before; @@ -163,26 +162,6 @@ public class BatteryInfoTest { assertThat(info2.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX); } - @Test - public void testGetBatteryInfo_basedOnUsageTrueLessThanSevenMinutes_usesCorrectString() { - Estimate estimate = new Estimate(Duration.ofMinutes(7).toMillis(), - true /* isBasedOnUsage */, - 1000 /* averageDischargeTime */); - BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, - mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000, - false /* shortString */); - BatteryInfo info2 = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, - mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000, - true /* shortString */); - - // These should be identical in either case - assertThat(info.remainingLabel.toString()).isEqualTo( - mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent)); - assertThat(info2.remainingLabel.toString()).isEqualTo( - mContext.getString(R.string.power_remaining_duration_only_shutdown_imminent)); - assertThat(info2.suggestionLabel).contains(BATTERY_RUN_OUT_PREFIX); - } - @Test @Ignore public void getBatteryInfo_MoreThanOneDay_suggestionLabelIsCorrectString() { @@ -196,25 +175,6 @@ public class BatteryInfoTest { assertThat(info.suggestionLabel).doesNotContain(BATTERY_RUN_OUT_PREFIX); } - @Test - public void - testGetBatteryInfo_basedOnUsageTrueBetweenSevenAndFifteenMinutes_usesCorrectString() { - Estimate estimate = new Estimate(Duration.ofMinutes(10).toMillis(), - true /* isBasedOnUsage */, - 1000 /* averageDischargeTime */); - BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, - mBatteryUsageStats, estimate, SystemClock.elapsedRealtime() * 1000, - false /* shortString */); - - // Check that strings are showing less than 15 minutes remaining regardless of exact time. - assertThat(info.chargeLabel.toString()).isEqualTo( - mContext.getString(R.string.power_remaining_less_than_duration, - FIFTEEN_MIN_FORMATTED, TEST_BATTERY_LEVEL_10)); - assertThat(info.remainingLabel.toString()).isEqualTo( - mContext.getString(R.string.power_remaining_less_than_duration_only, - FIFTEEN_MIN_FORMATTED)); - } - @Test public void testGetBatteryInfo_basedOnUsageFalse_usesDefaultString() { BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mDisChargingBatteryBroadcast, From 6968e32ed9489215b8aab081b7439eb545ff21f3 Mon Sep 17 00:00:00 2001 From: Avinash Vadlamudi Date: Fri, 12 May 2023 12:18:03 +0000 Subject: [PATCH 19/21] [Auto Pin Confirmation]: Fix the color token for the checkbox fill icon Bug: 281946128 Test: Manual test by flashing and testing on device Change-Id: I182e530bc39620582a0c228ab3d5934638474da2 --- res/drawable/ic_check_circle_filled_24dp.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/drawable/ic_check_circle_filled_24dp.xml b/res/drawable/ic_check_circle_filled_24dp.xml index 9d2e296ef2f..507bf678ae2 100644 --- a/res/drawable/ic_check_circle_filled_24dp.xml +++ b/res/drawable/ic_check_circle_filled_24dp.xml @@ -20,7 +20,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?androidprv:attr/materialColorPrimaryContainer"> + android:tint="?android:attr/colorPrimary"> From 852404809408afedf88ebe99541da9e91e3dfe1c Mon Sep 17 00:00:00 2001 From: Charles Chen Date: Wed, 10 May 2023 17:07:15 +0800 Subject: [PATCH 20/21] Improve Settings launch performance for normal phones 1. Initialize ActivitEmbedding component only if necessary 2. Early return to avoid executing long execution time operations ex: initialize ActivityEmbedding component, feature flag operations Test: manual - launch settings and profile Test: run v2/android-crystalball-eng/health/microbench/startup/firstparty/open-settings on affacted devices Test: atest SettingsHomepageActivityTest TopLevelWallpaperPreferenceControllerTest DashboardFeatureProviderImplTest TopLevelSettingsTest TopLevelWallpaperPreferenceControllerTest SearchResultTrampolineTest Test: atest CtsSettingsTestCases Fixes: 281505190 Change-Id: I0c1a1dc50f26c4ded02de82190dd7aad59c20c01 --- .../android/settings/SettingsApplication.java | 5 +-- .../ActivityEmbeddingUtils.java | 33 +++++++++++-------- .../homepage/SettingsHomepageActivity.java | 14 ++++---- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/com/android/settings/SettingsApplication.java b/src/com/android/settings/SettingsApplication.java index 7d8055d6337..8c050ea2b55 100644 --- a/src/com/android/settings/SettingsApplication.java +++ b/src/com/android/settings/SettingsApplication.java @@ -51,8 +51,9 @@ public class SettingsApplication extends Application { // Set Spa environment. setSpaEnvironment(); - if (FeatureFlagUtils.isEnabled(this, FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN) - && ActivityEmbeddingUtils.isSettingsSplitEnabled(this)) { + if (ActivityEmbeddingUtils.isSettingsSplitEnabled(this) + && FeatureFlagUtils.isEnabled(this, + FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN)) { if (WizardManagerHelper.isUserSetupComplete(this)) { new ActivityEmbeddingRulesController(this).initRules(); } else { diff --git a/src/com/android/settings/activityembedding/ActivityEmbeddingUtils.java b/src/com/android/settings/activityembedding/ActivityEmbeddingUtils.java index ae890f87632..67d56e0393a 100644 --- a/src/com/android/settings/activityembedding/ActivityEmbeddingUtils.java +++ b/src/com/android/settings/activityembedding/ActivityEmbeddingUtils.java @@ -97,16 +97,24 @@ public class ActivityEmbeddingUtils { * */ public static boolean isEmbeddingActivityEnabled(Context context) { - boolean isFlagEnabled = FeatureFlagUtils.isEnabled(context, - FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN); - boolean isSettingsSplitSupported = isSettingsSplitEnabled(context); - boolean isUserSetupComplete = WizardManagerHelper.isUserSetupComplete(context); - - Log.d(TAG, "isFlagEnabled = " + isFlagEnabled); - Log.d(TAG, "isSettingsSplitSupported = " + isSettingsSplitSupported); - Log.d(TAG, "isUserSetupComplete = " + isUserSetupComplete); - - return isFlagEnabled && isSettingsSplitSupported && isUserSetupComplete; + // Activity Embedding feature is not enabled if Settings doesn't enable large screen + // optimization or the device is not supported. + if (!isSettingsSplitEnabled(context)) { + Log.d(TAG, "isSettingsSplitSupported = false"); + return false; + } + // Activity Embedding feature is not enabled if a user chooses to disable the feature. + if (!FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN)) { + Log.d(TAG, "isFlagEnabled = false"); + return false; + } + // Don't enable Activity embedding for setup wizard. + if (!WizardManagerHelper.isUserSetupComplete(context)) { + Log.d(TAG, "isUserSetupComplete = false"); + return false; + } + Log.d(TAG, "isEmbeddingActivityEnabled = true"); + return true; } /** Whether to show the regular or simplified homepage layout. */ @@ -120,8 +128,7 @@ public class ActivityEmbeddingUtils { * Check if activity is already embedded */ public static boolean isAlreadyEmbedded(Activity activity) { - return ActivityEmbeddingController - .getInstance(activity) - .isActivityEmbedded(activity); + return isEmbeddingActivityEnabled(activity) && ActivityEmbeddingController.getInstance( + activity).isActivityEmbedded(activity); } } diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 03bc1b32fb2..829a89c6f03 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -57,7 +57,6 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import androidx.window.embedding.ActivityEmbeddingController; import androidx.window.embedding.SplitRule; import com.android.settings.R; @@ -108,7 +107,6 @@ public class SettingsHomepageActivity extends FragmentActivity implements private View mTwoPaneSuggestionView; private CategoryMixin mCategoryMixin; private Set mLoadedListeners; - private ActivityEmbeddingController mActivityEmbeddingController; private boolean mIsEmbeddingActivityEnabled; private boolean mIsTwoPane; // A regular layout shows icons on homepage, whereas a simplified layout doesn't. @@ -200,8 +198,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements setupEdgeToEdge(); setContentView(R.layout.settings_homepage_container); - mActivityEmbeddingController = ActivityEmbeddingController.getInstance(this); - mIsTwoPane = mActivityEmbeddingController.isActivityEmbedded(this); + mIsTwoPane = ActivityEmbeddingUtils.isAlreadyEmbedded(this); updateAppBarMinHeight(); initHomepageContainer(); @@ -242,7 +239,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements // Settings app may be launched on an existing task. Reset SplitPairRule of SubSettings here // to prevent SplitPairRule of an existing task applied on a new started Settings app. - if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this) + if (mIsEmbeddingActivityEnabled && (getIntent().getFlags() & Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) { initSplitPairRules(); } @@ -284,7 +281,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - final boolean newTwoPaneState = mActivityEmbeddingController.isActivityEmbedded(this); + final boolean newTwoPaneState = ActivityEmbeddingUtils.isAlreadyEmbedded(this); if (mIsTwoPane != newTwoPaneState) { mIsTwoPane = newTwoPaneState; updateHomepageAppBar(); @@ -427,8 +424,9 @@ public class SettingsHomepageActivity extends FragmentActivity implements } private boolean shouldLaunchDeepLinkIntentToRight() { - if (!FeatureFlagUtils.isEnabled(this, FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN) - || !ActivityEmbeddingUtils.isSettingsSplitEnabled(this)) { + if (!ActivityEmbeddingUtils.isSettingsSplitEnabled(this) + || !FeatureFlagUtils.isEnabled(this, + FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN)) { return false; } From 0aa67ede68d72d3ce9d2608e16eee207212221f7 Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Fri, 12 May 2023 03:32:34 +0000 Subject: [PATCH 21/21] chore(#AlwaysOnMagnification): hardcode the feature flag default true Set the feature flag getter default return value to be true to rollout to public. We keep the checking code so that it's still remote controllable. Bug: 281788002 Test: manually test with adb command Change-Id: Ie7f32721cf99d8a97c11f0f9c6d0d466c083d668 --- ...oggleScreenMagnificationPreferenceFragment.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 201172575ed..12706712427 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -249,12 +249,18 @@ public class ToggleScreenMagnificationPreferenceFragment extends super.onProcessArguments(arguments); } - private void addAlwaysOnSetting(PreferenceCategory generalCategory) { - if (!DeviceConfig.getBoolean( + private boolean isAlwaysOnSettingEnabled() { + final boolean defaultValue = getContext().getResources().getBoolean( + com.android.internal.R.bool.config_magnification_always_on_enabled); + + return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_WINDOW_MANAGER, "AlwaysOnMagnifier__enable_always_on_magnifier", - false - )) { + defaultValue + ); + } + private void addAlwaysOnSetting(PreferenceCategory generalCategory) { + if (!isAlwaysOnSettingEnabled()) { return; }