From 11081cae501808b6b5c52986b72da8ca067e4c88 Mon Sep 17 00:00:00 2001 From: pingzhi wang Date: Thu, 11 Aug 2022 16:34:59 +0800 Subject: [PATCH 01/11] Refresh "Device details" after resuming "HD Audio: LDAC" is still on when relaunch "Device details" from recent apps after turning it off Call refresh() after resuming. Bug: 242351058 Test: 1.Long click "Bluetooth" form QS. 2.Pair and connect with IOT supporting LDAC. 3.Click the settings icon to the right of the connected device. 4.Turn on "HD audio: LDAC". 5.Click home key, make it in recent apps. 6.Settings-> Connected devices -> Media devices -> settings icon. 7.Turn off "HD audio: LDAC". 8.Try to relaunch the first "Device details" screen (step 5) from recent apps. Change-Id: I4a0c475211669f61e718f47a713a982ac58e914a --- .../settings/bluetooth/BluetoothDetailsProfilesController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java index 6f39121af34..87770d2202d 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java @@ -456,6 +456,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll item.registerCallback(this); } mProfileManager.addServiceListener(this); + refresh(); } @Override From d8c6dd4fe8d6115e5c529f4908e19eca924c1926 Mon Sep 17 00:00:00 2001 From: Ken Lin Date: Tue, 21 Mar 2023 07:29:50 +0000 Subject: [PATCH 02/11] Correct the resolution illustration at the setting page Replace xml with wrong description with correct one Bug: 272650243 Test: check Setting -> Display -> Screen resolution Change-Id: I878356800e965b19bb9064615d220212b35cca08 --- ...screen_resolution_1080p.xml => screen_resolution_full.xml} | 0 ...screen_resolution_1440p.xml => screen_resolution_high.xml} | 0 .../android/settings/display/ScreenResolutionFragment.java | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename res/drawable/{screen_resolution_1080p.xml => screen_resolution_full.xml} (100%) rename res/drawable/{screen_resolution_1440p.xml => screen_resolution_high.xml} (100%) diff --git a/res/drawable/screen_resolution_1080p.xml b/res/drawable/screen_resolution_full.xml similarity index 100% rename from res/drawable/screen_resolution_1080p.xml rename to res/drawable/screen_resolution_full.xml diff --git a/res/drawable/screen_resolution_1440p.xml b/res/drawable/screen_resolution_high.xml similarity index 100% rename from res/drawable/screen_resolution_1440p.xml rename to res/drawable/screen_resolution_high.xml diff --git a/src/com/android/settings/display/ScreenResolutionFragment.java b/src/com/android/settings/display/ScreenResolutionFragment.java index b40b48f246f..de7d25fefb9 100644 --- a/src/com/android/settings/display/ScreenResolutionFragment.java +++ b/src/com/android/settings/display/ScreenResolutionFragment.java @@ -251,10 +251,10 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { if (TextUtils.equals( mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX], key)) { - preference.setLottieAnimationResId(R.drawable.screen_resolution_1080p); + preference.setLottieAnimationResId(R.drawable.screen_resolution_high); } else if (TextUtils.equals( mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX], key)) { - preference.setLottieAnimationResId(R.drawable.screen_resolution_1440p); + preference.setLottieAnimationResId(R.drawable.screen_resolution_full); } } From 5310c406400f71c2c540f23903a6766fbf58db1a Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Thu, 23 Mar 2023 15:30:19 +0800 Subject: [PATCH 03/11] Refine permission check process of 2-pane deep link - Check the deep link activity instance before rediercting to the internal activity for the managed profile invocation so the caller can't bypass the permission check. - Get the referrer as the caller so that onNewIntent can recognize the new caller and check if it has a permission to open the target page. Test: robotest & manual Bug: 268193384 Bug: 272437506 Change-Id: Ie69742983fb74ee2316b7aad16461db95ed927c2 --- .../homepage/SettingsHomepageActivity.java | 94 +++++++++++++------ .../SettingsHomepageActivityTest.java | 89 +++++++++++++++--- 2 files changed, 142 insertions(+), 41 deletions(-) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index fac3aafe152..7713e270425 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -29,11 +29,12 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; import android.os.Process; -import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; @@ -71,7 +72,6 @@ import com.android.settings.core.CategoryMixin; import com.android.settings.core.FeatureFlags; import com.android.settings.homepage.contextualcards.ContextualCardsFragment; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.password.PasswordUtils; import com.android.settings.safetycenter.SafetyCenterManagerWrapper; import com.android.settingslib.Utils; import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; @@ -96,6 +96,10 @@ public class SettingsHomepageActivity extends FragmentActivity implements public static final String EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA = "settings_large_screen_deep_link_intent_data"; + // The referrer who fires the initial intent to start the homepage + @VisibleForTesting + static final String EXTRA_INITIAL_REFERRER = "initial_referrer"; + static final int DEFAULT_HIGHLIGHT_MENU_KEY = R.string.menu_key_network; private static final long HOMEPAGE_LOADING_TIMEOUT_MS = 300; @@ -177,12 +181,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); if (mIsEmbeddingActivityEnabled) { final UserManager um = getSystemService(UserManager.class); - final UserInfo userInfo = um.getUserInfo(getUser().getIdentifier()); + final UserInfo userInfo = um.getUserInfo(getUserId()); if (userInfo.isManagedProfile()) { final Intent intent = new Intent(getIntent()) - .setClass(this, DeepLinkHomepageActivityInternal.class) .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) - .putExtra(EXTRA_USER_HANDLE, getUser()); + .putExtra(EXTRA_USER_HANDLE, getUser()) + .putExtra(EXTRA_INITIAL_REFERRER, getCurrentReferrer()); + if (TextUtils.equals(intent.getAction(), ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY) + && this instanceof DeepLinkHomepageActivity) { + intent.setClass(this, DeepLinkHomepageActivityInternal.class); + } intent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivityAsUser(intent, um.getProfileParent(userInfo.id).getUserHandle()); finish(); @@ -471,7 +479,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements return; } - ActivityInfo targetActivityInfo = null; + ActivityInfo targetActivityInfo; try { targetActivityInfo = getPackageManager().getActivityInfo(targetComponentName, /* flags= */ 0); @@ -481,23 +489,29 @@ public class SettingsHomepageActivity extends FragmentActivity implements return; } - int callingUid = -1; - try { - callingUid = ActivityManager.getService().getLaunchedFromUid(getActivityToken()); - } catch (RemoteException re) { - Log.e(TAG, "Not able to get callingUid: " + re); - finish(); - return; + UserHandle user = intent.getParcelableExtra(EXTRA_USER_HANDLE, UserHandle.class); + String caller = getInitialReferrer(); + int callerUid = -1; + if (caller != null) { + try { + callerUid = getPackageManager().getApplicationInfoAsUser(caller, + ApplicationInfoFlags.of(/* flags= */ 0), + user != null ? user.getIdentifier() : getUserId()).uid; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Not able to get callerUid: " + e); + finish(); + return; + } } - if (!hasPrivilegedAccess(callingUid, targetActivityInfo)) { + if (!hasPrivilegedAccess(caller, callerUid, targetActivityInfo.packageName)) { if (!targetActivityInfo.exported) { Log.e(TAG, "Target Activity is not exported"); finish(); return; } - if (!isCallingAppPermitted(targetActivityInfo.permission)) { + if (!isCallingAppPermitted(targetActivityInfo.permission, callerUid)) { Log.e(TAG, "Calling app must have the permission of deep link Activity"); finish(); return; @@ -510,7 +524,7 @@ public class SettingsHomepageActivity extends FragmentActivity implements & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); if (targetIntent.getData() != null && uriPermissionFlags != 0 - && checkUriPermission(targetIntent.getData(), /* pid= */ -1, callingUid, + && checkUriPermission(targetIntent.getData(), /* pid= */ -1, callerUid, uriPermissionFlags) == PackageManager.PERMISSION_DENIED) { Log.e(TAG, "Calling app must have the permission to access Uri and grant permission"); finish(); @@ -547,7 +561,6 @@ public class SettingsHomepageActivity extends FragmentActivity implements SplitRule.FinishBehavior.ALWAYS, true /* clearTop */); - final UserHandle user = intent.getParcelableExtra(EXTRA_USER_HANDLE, UserHandle.class); if (user != null) { startActivityAsUser(targetIntent, user); } else { @@ -555,31 +568,30 @@ public class SettingsHomepageActivity extends FragmentActivity implements } } - // Check if calling app has privileged access to launch Activity of activityInfo. - private boolean hasPrivilegedAccess(int callingUid, ActivityInfo activityInfo) { - if (TextUtils.equals(PasswordUtils.getCallingAppPackageName(getActivityToken()), - getPackageName())) { + // Check if the caller has privileged access to launch the target page. + private boolean hasPrivilegedAccess(String callerPkg, int callerUid, String targetPackage) { + if (TextUtils.equals(callerPkg, getPackageName())) { return true; } int targetUid = -1; try { - targetUid = getPackageManager().getApplicationInfo(activityInfo.packageName, - /* flags= */ 0).uid; - } catch (PackageManager.NameNotFoundException nnfe) { - Log.e(TAG, "Not able to get targetUid: " + nnfe); + targetUid = getPackageManager().getApplicationInfo(targetPackage, + ApplicationInfoFlags.of(/* flags= */ 0)).uid; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Not able to get targetUid: " + e); return false; } // When activityInfo.exported is false, Activity still can be launched if applications have // the same user ID. - if (UserHandle.isSameApp(callingUid, targetUid)) { + if (UserHandle.isSameApp(callerUid, targetUid)) { return true; } // When activityInfo.exported is false, Activity still can be launched if calling app has // root or system privilege. - int callingAppId = UserHandle.getAppId(callingUid); + int callingAppId = UserHandle.getAppId(callerUid); if (callingAppId == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID) { return true; } @@ -588,9 +600,31 @@ public class SettingsHomepageActivity extends FragmentActivity implements } @VisibleForTesting - boolean isCallingAppPermitted(String permission) { - return TextUtils.isEmpty(permission) || PasswordUtils.isCallingAppPermitted( - this, getActivityToken(), permission); + String getInitialReferrer() { + String referrer = getCurrentReferrer(); + if (!TextUtils.equals(referrer, getPackageName())) { + return referrer; + } + + String initialReferrer = getIntent().getStringExtra(EXTRA_INITIAL_REFERRER); + return TextUtils.isEmpty(initialReferrer) ? referrer : initialReferrer; + } + + @VisibleForTesting + String getCurrentReferrer() { + Intent intent = getIntent(); + // Clear extras to get the real referrer + intent.removeExtra(Intent.EXTRA_REFERRER); + intent.removeExtra(Intent.EXTRA_REFERRER_NAME); + Uri referrer = getReferrer(); + return referrer != null ? referrer.getHost() : null; + } + + @VisibleForTesting + boolean isCallingAppPermitted(String permission, int callerUid) { + return TextUtils.isEmpty(permission) + || checkPermission(permission, /* pid= */ -1, callerUid) + == PackageManager.PERMISSION_GRANTED; } private String getHighlightMenuKey() { diff --git a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java index 60f153cf233..f1d6796d775 100644 --- a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java +++ b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java @@ -20,8 +20,13 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -30,6 +35,8 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.view.View; import android.view.Window; @@ -215,29 +222,89 @@ public class SettingsHomepageActivityTest { } @Test - @Config(shadows = {ShadowPasswordUtils.class}) + public void getInitialReferrer_differentPackage_returnCurrentReferrer() { + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + String referrer = "com.abc"; + doReturn(referrer).when(activity).getCurrentReferrer(); + + assertEquals(activity.getInitialReferrer(), referrer); + } + + @Test + public void getInitialReferrer_noReferrerExtra_returnCurrentReferrer() { + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + String referrer = activity.getPackageName(); + doReturn(referrer).when(activity).getCurrentReferrer(); + + assertEquals(activity.getInitialReferrer(), referrer); + } + + @Test + public void getInitialReferrer_hasReferrerExtra_returnGivenReferrer() { + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + doReturn(activity.getPackageName()).when(activity).getCurrentReferrer(); + String referrer = "com.abc"; + activity.setIntent(new Intent().putExtra(SettingsHomepageActivity.EXTRA_INITIAL_REFERRER, + referrer)); + + assertEquals(activity.getInitialReferrer(), referrer); + } + + @Test + public void getCurrentReferrer_hasReferrerExtra_shouldNotEqual() { + String referrer = "com.abc"; + Uri uri = new Uri.Builder().scheme("android-app").authority(referrer).build(); + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + activity.setIntent(new Intent().putExtra(Intent.EXTRA_REFERRER, uri)); + + assertNotEquals(activity.getCurrentReferrer(), referrer); + } + + @Test + public void getCurrentReferrer_hasReferrerNameExtra_shouldNotEqual() { + String referrer = "com.abc"; + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + activity.setIntent(new Intent().putExtra(Intent.EXTRA_REFERRER_NAME, referrer)); + + assertNotEquals(activity.getCurrentReferrer(), referrer); + } + + @Test public void isCallingAppPermitted_emptyPermission_returnTrue() { - SettingsHomepageActivity homepageActivity = spy(new SettingsHomepageActivity()); + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + doReturn(PackageManager.PERMISSION_DENIED).when(activity) + .checkPermission(anyString(), anyInt(), anyInt()); - assertTrue(homepageActivity.isCallingAppPermitted("")); + assertTrue(activity.isCallingAppPermitted("", 1000)); } @Test - @Config(shadows = {ShadowPasswordUtils.class}) - public void isCallingAppPermitted_noGrantedPermission_returnFalse() { - SettingsHomepageActivity homepageActivity = spy(new SettingsHomepageActivity()); + public void isCallingAppPermitted_notGrantedPermission_returnFalse() { + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); + doReturn(PackageManager.PERMISSION_DENIED).when(activity) + .checkPermission(anyString(), anyInt(), anyInt()); - assertFalse(homepageActivity.isCallingAppPermitted("android.permission.TEST")); + assertFalse(activity.isCallingAppPermitted("android.permission.TEST", 1000)); } @Test - @Config(shadows = {ShadowPasswordUtils.class}) public void isCallingAppPermitted_grantedPermission_returnTrue() { - SettingsHomepageActivity homepageActivity = spy(new SettingsHomepageActivity()); + SettingsHomepageActivity activity = + spy(Robolectric.buildActivity(SettingsHomepageActivity.class).get()); String permission = "android.permission.TEST"; - ShadowPasswordUtils.addGrantedPermission(permission); + doReturn(PackageManager.PERMISSION_DENIED).when(activity) + .checkPermission(anyString(), anyInt(), anyInt()); + doReturn(PackageManager.PERMISSION_GRANTED).when(activity) + .checkPermission(eq(permission), anyInt(), eq(1000)); - assertTrue(homepageActivity.isCallingAppPermitted(permission)); + assertTrue(activity.isCallingAppPermitted(permission, 1000)); } @Implements(SuggestionFeatureProviderImpl.class) From 79588ff434ef10ce8d83910cee24db4cecfa4055 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Mon, 27 Mar 2023 08:02:03 +0000 Subject: [PATCH 04/11] Changes screen flash color selected ring color in Flash Notifications There's an outer ring to show that the screen flash color is selected and it's black in both light and dark themes. When selecting screen flash color in the dark theme, it's not obvious which color is selected because the indicated ring is black which is similar to the dark background. Changing it to white in the dark theme for better user experience. Bug: 275283917 Test: checks UI manually Change-Id: I714597d99dfb74c5ce0ddef3995fd19688feacdf --- res/values-night/colors.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml index 959bc172793..aae7403c835 100644 --- a/res/values-night/colors.xml +++ b/res/values-night/colors.xml @@ -67,5 +67,9 @@ #7DA7F1 #607DA7F1 #FFEE675C + + + + #FFFFFF From 793257967f165970f8cb0f4cebddab9dcd5d8353 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 7 Mar 2023 15:44:29 -0500 Subject: [PATCH 05/11] Don't show NLSes with excessively long component names Test: install test app with long CN Test: ServiceListingTest Bug: 260570119 Change-Id: I3ffd02f6cf6bf282e7fc264fd070ed3add4d8571 --- .../settings/notification/NotificationAccessSettings.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/com/android/settings/notification/NotificationAccessSettings.java b/src/com/android/settings/notification/NotificationAccessSettings.java index 4ec9ccd8144..56d3f0e445c 100644 --- a/src/com/android/settings/notification/NotificationAccessSettings.java +++ b/src/com/android/settings/notification/NotificationAccessSettings.java @@ -65,6 +65,7 @@ public class NotificationAccessSettings extends EmptyTextSettings { private static final String TAG = "NotifAccessSettings"; private static final String ALLOWED_KEY = "allowed"; private static final String NOT_ALLOWED_KEY = "not_allowed"; + private static final int MAX_CN_LENGTH = 500; private static final ManagedServiceSettings.Config CONFIG = new ManagedServiceSettings.Config.Builder() @@ -101,6 +102,12 @@ public class NotificationAccessSettings extends EmptyTextSettings { .setNoun(CONFIG.noun) .setSetting(CONFIG.setting) .setTag(CONFIG.tag) + .setValidator(info -> { + if (info.getComponentName().flattenToString().length() > MAX_CN_LENGTH) { + return false; + } + return true; + }) .build(); mServiceListing.addCallback(this::updateList); From 90ced5d2ecdb13d7d8f4e4052b4bd30cc2a37979 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 7 Mar 2023 15:44:29 -0500 Subject: [PATCH 06/11] Don't show NLSes with excessively long component names Test: install test app with long CN Test: ServiceListingTest Bug: 260570119 Change-Id: I3ffd02f6cf6bf282e7fc264fd070ed3add4d8571 --- .../settings/notification/NotificationAccessSettings.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/com/android/settings/notification/NotificationAccessSettings.java b/src/com/android/settings/notification/NotificationAccessSettings.java index 10954a185e1..d94498e68f7 100644 --- a/src/com/android/settings/notification/NotificationAccessSettings.java +++ b/src/com/android/settings/notification/NotificationAccessSettings.java @@ -62,6 +62,7 @@ public class NotificationAccessSettings extends EmptyTextSettings { private static final String TAG = "NotifAccessSettings"; private static final String ALLOWED_KEY = "allowed"; private static final String NOT_ALLOWED_KEY = "not_allowed"; + private static final int MAX_CN_LENGTH = 500; private static final ManagedServiceSettings.Config CONFIG = new ManagedServiceSettings.Config.Builder() @@ -98,6 +99,12 @@ public class NotificationAccessSettings extends EmptyTextSettings { .setNoun(CONFIG.noun) .setSetting(CONFIG.setting) .setTag(CONFIG.tag) + .setValidator(info -> { + if (info.getComponentName().flattenToString().length() > MAX_CN_LENGTH) { + return false; + } + return true; + }) .build(); mServiceListing.addCallback(this::updateList); From 1f3969f615ee501ee61fd920be1e13dbef0acbb5 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 7 Mar 2023 15:44:29 -0500 Subject: [PATCH 07/11] Don't show NLSes with excessively long component names Test: install test app with long CN Test: ServiceListingTest Bug: 260570119 Change-Id: I3ffd02f6cf6bf282e7fc264fd070ed3add4d8571 --- .../settings/notification/NotificationAccessSettings.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/com/android/settings/notification/NotificationAccessSettings.java b/src/com/android/settings/notification/NotificationAccessSettings.java index 10954a185e1..d94498e68f7 100644 --- a/src/com/android/settings/notification/NotificationAccessSettings.java +++ b/src/com/android/settings/notification/NotificationAccessSettings.java @@ -62,6 +62,7 @@ public class NotificationAccessSettings extends EmptyTextSettings { private static final String TAG = "NotifAccessSettings"; private static final String ALLOWED_KEY = "allowed"; private static final String NOT_ALLOWED_KEY = "not_allowed"; + private static final int MAX_CN_LENGTH = 500; private static final ManagedServiceSettings.Config CONFIG = new ManagedServiceSettings.Config.Builder() @@ -98,6 +99,12 @@ public class NotificationAccessSettings extends EmptyTextSettings { .setNoun(CONFIG.noun) .setSetting(CONFIG.setting) .setTag(CONFIG.tag) + .setValidator(info -> { + if (info.getComponentName().flattenToString().length() > MAX_CN_LENGTH) { + return false; + } + return true; + }) .build(); mServiceListing.addCallback(this::updateList); From aa33027c92bc5cd178e4ca89adc841168bf67442 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Sat, 25 Mar 2023 10:27:17 +0000 Subject: [PATCH 08/11] Handles Flash Notifications intro if there's no valid camera If there is no valid camera found on the device, the camera flash option will be hidden by default. It seems weired if the intro still mentions the camera light will flash while there's no camera flash option at all. Updates the intro string without mentioning camera light if there's no valid camera found. Bug: 274565006 Test: check the string on device without valid camera manually Change-Id: I93b1fe372fe9f9e2634e56b19b30898847d68c9c --- res/values/strings.xml | 2 + res/xml/flash_notifications_settings.xml | 4 +- ...otificationsIntroPreferenceController.java | 51 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/com/android/settings/accessibility/FlashNotificationsIntroPreferenceController.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 44f65034c70..1f1ecc820d3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11970,6 +11970,8 @@ On / Camera and screen flash Flash the camera light or the screen when you receive notifications or when alarms sound + + Flash the screen when you receive notifications or when alarms sound Use flash notifications with caution if you\u0027re light sensitive diff --git a/res/xml/flash_notifications_settings.xml b/res/xml/flash_notifications_settings.xml index 0017fa605f5..243fffc02a8 100644 --- a/res/xml/flash_notifications_settings.xml +++ b/res/xml/flash_notifications_settings.xml @@ -21,12 +21,12 @@ + settings:controller="com.android.settings.accessibility.FlashNotificationsIntroPreferenceController" /> + settings:lottie_rawRes="@drawable/flash_notifications_illustration" /> Date: Mon, 20 Mar 2023 08:17:26 +0000 Subject: [PATCH 09/11] Create tooltip for notifying auto-adding the font scaling tile 1. Add string for the content of tooltip. 2. Show the tooltip if needed: the tooltip will only be shown once when users change the font size from the Settings page for the first time. 3. Since the layout shown on the screen will be recreated after font size changes, we need to save the state of the tooltip popup window to check if we need to reshow it in displayPreference. Bug: 269679768 Test: Manually - attach videos to the bug Test: make RunSettingsRoboTests ROBOTEST_FILTER=PreviewSizeSeekBarControllerTest Change-Id: I1b6c5fdbd74c7a868cf42bd21d2cdb1052c0bbe6 --- res/values/strings.xml | 2 + .../PreviewSizeSeekBarController.java | 87 +++++++++++- .../TextReadingPreferenceFragment.java | 28 +++- .../widget/LabeledSeekBarPreference.java | 14 +- .../PreviewSizeSeekBarControllerTest.java | 125 +++++++++++++++++- 5 files changed, 242 insertions(+), 14 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 3e96e1de6e5..4f6faf12d16 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4567,6 +4567,8 @@ One-handed mode added to Quick Settings. Swipe down to turn it on or off anytime. You can also add one-handed mode to Quick Settings from the top of your screen + + Font size added to Quick Settings. Swipe down to change the font size anytime. Dismiss diff --git a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java index 851797ec2a9..089dc7be535 100644 --- a/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java +++ b/src/com/android/settings/accessibility/PreviewSizeSeekBarController.java @@ -16,14 +16,22 @@ package com.android.settings.accessibility; +import android.content.ComponentName; import android.content.Context; +import android.os.Bundle; +import android.os.Handler; import android.widget.SeekBar; import androidx.annotation.NonNull; import androidx.preference.PreferenceScreen; +import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.widget.LabeledSeekBarPreference; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreate; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; import java.util.Optional; @@ -31,12 +39,19 @@ import java.util.Optional; * The controller of {@link LabeledSeekBarPreference} that listens to display size and font size * settings changes and updates preview size threshold smoothly. */ -class PreviewSizeSeekBarController extends BasePreferenceController implements - TextReadingResetController.ResetStateListener { +abstract class PreviewSizeSeekBarController extends BasePreferenceController implements + TextReadingResetController.ResetStateListener, LifecycleObserver, OnCreate, + OnDestroy, OnSaveInstanceState { private final PreviewSizeData mSizeData; + private static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; private boolean mSeekByTouch; private Optional mInteractionListener = Optional.empty(); private LabeledSeekBarPreference mSeekBarPreference; + private int mLastProgress; + private boolean mNeedsQSTooltipReshow = false; + private AccessibilityQuickSettingsTooltipWindow mTooltipWindow; + private final Handler mHandler; + private final SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @@ -54,6 +69,7 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements if (!mSeekByTouch) { interactionListener.onProgressChanged(); + onProgressFinalized(); } } @@ -67,6 +83,7 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements mSeekByTouch = false; mInteractionListener.ifPresent(ProgressInteractionListener::onEndTrackingTouch); + onProgressFinalized(); } }; @@ -74,6 +91,30 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements @NonNull PreviewSizeData sizeData) { super(context, preferenceKey); mSizeData = sizeData; + mHandler = new Handler(context.getMainLooper()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + // Restore the tooltip. + if (savedInstanceState != null + && savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) { + mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW); + } + } + + @Override + public void onDestroy() { + // remove runnables in the queue. + mHandler.removeCallbacksAndMessages(null); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); + if (mNeedsQSTooltipReshow || isTooltipWindowShowing) { + outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); + } } void setInteractionListener(ProgressInteractionListener interactionListener) { @@ -91,11 +132,15 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements final int dataSize = mSizeData.getValues().size(); final int initialIndex = mSizeData.getInitialIndex(); + mLastProgress = initialIndex; mSeekBarPreference = screen.findPreference(getPreferenceKey()); mSeekBarPreference.setMax(dataSize - 1); mSeekBarPreference.setProgress(initialIndex); mSeekBarPreference.setContinuousUpdates(true); mSeekBarPreference.setOnSeekBarChangeListener(mSeekBarChangeListener); + if (mNeedsQSTooltipReshow) { + mHandler.post(this::showQuickSettingsTooltipIfNeeded); + } } @Override @@ -108,6 +153,44 @@ class PreviewSizeSeekBarController extends BasePreferenceController implements mInteractionListener.ifPresent(ProgressInteractionListener::onProgressChanged); } + private void onProgressFinalized() { + // Using progress in SeekBarPreference since the progresses in + // SeekBarPreference and seekbar are not always the same. + // See {@link androidx.preference.Preference#callChangeListener(Object)} + int seekBarPreferenceProgress = mSeekBarPreference.getProgress(); + if (seekBarPreferenceProgress != mLastProgress) { + showQuickSettingsTooltipIfNeeded(); + mLastProgress = seekBarPreferenceProgress; + } + } + + private void showQuickSettingsTooltipIfNeeded() { + final ComponentName tileComponentName = getTileComponentName(); + if (tileComponentName == null) { + // Returns if no tile service assigned. + return; + } + + if (!mNeedsQSTooltipReshow && AccessibilityQuickSettingUtils.hasValueInSharedPreferences( + mContext, tileComponentName)) { + // Returns if quick settings tooltip only show once. + return; + } + + mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(mContext); + mTooltipWindow.setup(getTileTooltipContent(), + R.drawable.accessibility_auto_added_qs_tooltip_illustration); + mTooltipWindow.showAtTopCenter(mSeekBarPreference.getSeekbar()); + AccessibilityQuickSettingUtils.optInValueToSharedPreferences(mContext, tileComponentName); + mNeedsQSTooltipReshow = false; + } + + /** Returns the accessibility Quick Settings tile component name. */ + abstract ComponentName getTileComponentName(); + + /** Returns accessibility Quick Settings tile tooltip content. */ + abstract CharSequence getTileTooltipContent(); + /** * Interface for callbacks when users interact with the seek bar. diff --git a/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java b/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java index b35a5fe5a24..97a9071066e 100644 --- a/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java +++ b/src/com/android/settings/accessibility/TextReadingPreferenceFragment.java @@ -16,11 +16,13 @@ package com.android.settings.accessibility; +import static com.android.internal.accessibility.AccessibilityShortcutController.FONT_SIZE_COMPONENT_NAME; import static com.android.settings.accessibility.TextReadingResetController.ResetStateListener; import android.app.Activity; import android.app.Dialog; import android.app.settings.SettingsEnums; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; @@ -156,12 +158,34 @@ public class TextReadingPreferenceFragment extends DashboardFragment { controllers.add(mPreviewController); final PreviewSizeSeekBarController fontSizeController = new PreviewSizeSeekBarController( - context, FONT_SIZE_KEY, fontSizeData); + context, FONT_SIZE_KEY, fontSizeData) { + @Override + ComponentName getTileComponentName() { + return FONT_SIZE_COMPONENT_NAME; + } + + @Override + CharSequence getTileTooltipContent() { + return context.getText( + R.string.accessibility_font_scaling_auto_added_qs_tooltip_content); + } + }; fontSizeController.setInteractionListener(mPreviewController); + getSettingsLifecycle().addObserver(fontSizeController); controllers.add(fontSizeController); final PreviewSizeSeekBarController displaySizeController = new PreviewSizeSeekBarController( - context, DISPLAY_SIZE_KEY, displaySizeData); + context, DISPLAY_SIZE_KEY, displaySizeData) { + @Override + ComponentName getTileComponentName() { + return null; + } + + @Override + CharSequence getTileTooltipContent() { + return null; + } + }; displaySizeController.setInteractionListener(mPreviewController); controllers.add(displaySizeController); diff --git a/src/com/android/settings/widget/LabeledSeekBarPreference.java b/src/com/android/settings/widget/LabeledSeekBarPreference.java index 5d1011634c7..6300bd3b318 100644 --- a/src/com/android/settings/widget/LabeledSeekBarPreference.java +++ b/src/com/android/settings/widget/LabeledSeekBarPreference.java @@ -63,6 +63,8 @@ public class LabeledSeekBarPreference extends SeekBarPreference { private OnPreferenceChangeListener mStopListener; private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener; + private SeekBar mSeekBar; + public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -104,6 +106,10 @@ public class LabeledSeekBarPreference extends SeekBarPreference { com.android.internal.R.attr.seekBarPreferenceStyle), 0); } + public SeekBar getSeekbar() { + return mSeekBar; + } + @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); @@ -133,19 +139,19 @@ public class LabeledSeekBarPreference extends SeekBarPreference { final boolean isValidTextResIdExist = mTextStartId > 0 || mTextEndId > 0; labelFrame.setVisibility(isValidTextResIdExist ? View.VISIBLE : View.GONE); - final SeekBar seekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar); + mSeekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar); if (mTickMarkId != 0) { final Drawable tickMark = getContext().getDrawable(mTickMarkId); - seekBar.setTickMark(tickMark); + mSeekBar.setTickMark(tickMark); } final ViewGroup iconStartFrame = (ViewGroup) holder.findViewById(R.id.icon_start_frame); final ImageView iconStartView = (ImageView) holder.findViewById(R.id.icon_start); - updateIconStartIfNeeded(iconStartFrame, iconStartView, seekBar); + updateIconStartIfNeeded(iconStartFrame, iconStartView, mSeekBar); final ViewGroup iconEndFrame = (ViewGroup) holder.findViewById(R.id.icon_end_frame); final ImageView iconEndView = (ImageView) holder.findViewById(R.id.icon_end); - updateIconEndIfNeeded(iconEndFrame, iconEndView, seekBar); + updateIconEndIfNeeded(iconEndFrame, iconEndView, mSeekBar); } public void setOnPreferenceChangeStopListener(OnPreferenceChangeListener listener) { diff --git a/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java index 52ccb374ca6..6b0f5c00413 100644 --- a/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/PreviewSizeSeekBarControllerTest.java @@ -16,29 +16,45 @@ package com.android.settings.accessibility; +import static com.android.internal.accessibility.AccessibilityShortcutController.FONT_SIZE_COMPONENT_NAME; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.widget.PopupWindow; import android.widget.SeekBar; +import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.testutils.shadow.ShadowInteractionJankMonitor; import com.android.settings.widget.LabeledSeekBarPreference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowApplication; /** * Tests for {@link PreviewSizeSeekBarController}. @@ -47,30 +63,67 @@ import org.robolectric.annotation.Config; @Config(shadows = {ShadowInteractionJankMonitor.class}) public class PreviewSizeSeekBarControllerTest { private static final String FONT_SIZE_KEY = "font_size"; + private static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; + @Spy private final Context mContext = ApplicationProvider.getApplicationContext(); private PreviewSizeSeekBarController mSeekBarController; private FontSizeData mFontSizeData; private LabeledSeekBarPreference mSeekBarPreference; - @Mock private PreferenceScreen mPreferenceScreen; + private TestFragment mFragment; + private PreferenceViewHolder mHolder; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PreferenceManager mPreferenceManager; @Mock private PreviewSizeSeekBarController.ProgressInteractionListener mInteractionListener; + private static PopupWindow getLatestPopupWindow() { + final ShadowApplication shadowApplication = + Shadow.extract(ApplicationProvider.getApplicationContext()); + return shadowApplication.getLatestPopupWindow(); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); - mFontSizeData = new FontSizeData(mContext); - - mSeekBarController = - new PreviewSizeSeekBarController(mContext, FONT_SIZE_KEY, mFontSizeData); - + mContext.setTheme(R.style.Theme_AppCompat); + mFragment = spy(new TestFragment()); + when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mFragment.getPreferenceManager().getContext()).thenReturn(mContext); + when(mFragment.getContext()).thenReturn(mContext); + mPreferenceScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null)); + when(mPreferenceScreen.getPreferenceManager()).thenReturn(mPreferenceManager); + doReturn(mPreferenceScreen).when(mFragment).getPreferenceScreen(); mSeekBarPreference = spy(new LabeledSeekBarPreference(mContext, /* attrs= */ null)); + mSeekBarPreference.setKey(FONT_SIZE_KEY); + + LayoutInflater inflater = LayoutInflater.from(mContext); + mHolder = PreferenceViewHolder.createInstanceForTests(inflater.inflate( + R.layout.preference_labeled_slider, null)); + mSeekBarPreference.onBindViewHolder(mHolder); + when(mPreferenceScreen.findPreference(anyString())).thenReturn(mSeekBarPreference); + mFontSizeData = new FontSizeData(mContext); + mSeekBarController = + new PreviewSizeSeekBarController(mContext, FONT_SIZE_KEY, mFontSizeData) { + @Override + ComponentName getTileComponentName() { + return FONT_SIZE_COMPONENT_NAME; + } + + @Override + CharSequence getTileTooltipContent() { + return mContext.getText( + R.string.accessibility_font_scaling_auto_added_qs_tooltip_content); + } + }; mSeekBarController.setInteractionListener(mInteractionListener); + when(mPreferenceScreen.findPreference(mSeekBarController.getPreferenceKey())).thenReturn( + mSeekBarPreference); } @Test @@ -123,4 +176,64 @@ public class PreviewSizeSeekBarControllerTest { verify(mInteractionListener).notifyPreferenceChanged(); } + + @Test + public void onProgressChanged_showTooltipView() { + mSeekBarController.displayPreference(mPreferenceScreen); + + // Simulate changing the progress for the first time + int newProgress = (mSeekBarPreference.getProgress() != 0) ? 0 : mSeekBarPreference.getMax(); + mSeekBarPreference.setProgress(newProgress); + mSeekBarPreference.onProgressChanged(new SeekBar(mContext), + newProgress, + /* fromUser= */ false); + + assertThat(getLatestPopupWindow().isShowing()).isTrue(); + } + + @Test + public void onProgressChanged_tooltipViewHasBeenShown_notShowTooltipView() { + mSeekBarController.displayPreference(mPreferenceScreen); + // Simulate changing the progress for the first time + int newProgress = (mSeekBarPreference.getProgress() != 0) ? 0 : mSeekBarPreference.getMax(); + mSeekBarPreference.setProgress(newProgress); + mSeekBarPreference.onProgressChanged(new SeekBar(mContext), + newProgress, + /* fromUser= */ false); + getLatestPopupWindow().dismiss(); + + // Simulate progress changing for the second time + newProgress = (mSeekBarPreference.getProgress() != 0) ? 0 : mSeekBarPreference.getMax(); + mSeekBarPreference.setProgress(newProgress); + mSeekBarPreference.onProgressChanged(new SeekBar(mContext), + newProgress, + /* fromUser= */ false); + + assertThat(getLatestPopupWindow().isShowing()).isFalse(); + } + + @Test + @Config(shadows = ShadowFragment.class) + public void restoreValueFromSavedInstanceState_showTooltipView() { + final Bundle savedInstanceState = new Bundle(); + savedInstanceState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); + mSeekBarController.onCreate(savedInstanceState); + + mSeekBarController.displayPreference(mPreferenceScreen); + + assertThat(getLatestPopupWindow().isShowing()).isTrue(); + } + + private static class TestFragment extends SettingsPreferenceFragment { + + @Override + protected boolean shouldSkipForInitialSUW() { + return false; + } + + @Override + public int getMetricsCategory() { + return 0; + } + } } From a53b093a2bf266b0010ba2a14936730f2ecf3683 Mon Sep 17 00:00:00 2001 From: danielwbhuang Date: Tue, 28 Mar 2023 16:07:20 +0800 Subject: [PATCH 10/11] Change the "Restore" button to "Reset" "Restore" button should be displayed as "Reset", since the string "reset" is used more often and the action is actually "to reset". Bug: 274714167 Test: manual Change-Id: Ide6728bba8b17c22fe40b2dc0d65ea6166de44bc --- res/layout/modifier_key_reset_dialog.xml | 2 +- res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/layout/modifier_key_reset_dialog.xml b/res/layout/modifier_key_reset_dialog.xml index fd38b11d5a8..11712bf9960 100644 --- a/res/layout/modifier_key_reset_dialog.xml +++ b/res/layout/modifier_key_reset_dialog.xml @@ -80,7 +80,7 @@ android:drawablePadding="9dp" style="@style/ModifierKeyButtonCancel" android:textColor="?android:attr/textColorPrimary" - android:text="@string/modifier_keys_restore"/> + android:text="@string/modifier_keys_reset"/> \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 197672db02f..4ba9af45b2e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3982,7 +3982,7 @@ Cancel - Restore + Reset Choose modifier key From 84778453817c6f88f2cec478241cb77a1f86ba95 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Tue, 28 Mar 2023 20:01:39 +0800 Subject: [PATCH 11/11] [Regional prefernce] Remove U extension in locale of app list Bug: b/270251111 Test: Manual Test Change-Id: I2b3facfc5f7ec40a48379c79a5f0672cfe5fa7af --- .../android/settings/applications/appinfo/AppLocaleDetails.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java index 1a7793e2ead..6144a73821c 100644 --- a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java +++ b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java @@ -213,7 +213,7 @@ public class AppLocaleDetails extends SettingsPreferenceFragment { if (appLocale == null) { return context.getString(R.string.preference_of_system_locale_summary); } else { - return LocaleHelper.getDisplayName(appLocale, appLocale, true); + return LocaleHelper.getDisplayName(appLocale.stripExtensions(), appLocale, true); } } }