From 244a1c84712011955a5d1e204a2549ccea9e293e Mon Sep 17 00:00:00 2001 From: Adam Bookatz Date: Fri, 27 Aug 2021 11:54:30 -0700 Subject: [PATCH 01/11] Disable AppCopyFragment Test: manual confirmation Change-Id: Ifc2a879de64e152584cfeab2bc729463e3d5314d --- src/com/android/settings/users/UserDetailsSettings.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java index bd4c3e855fe..716ee0189e9 100644 --- a/src/com/android/settings/users/UserDetailsSettings.java +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -73,6 +73,9 @@ public class UserDetailsSettings extends SettingsPreferenceFragment private static final int DIALOG_SETUP_USER = 4; private static final int DIALOG_CONFIRM_RESET_GUEST = 5; + /** Whether to enable the app_copying fragment. */ + private static final boolean SHOW_APP_COPYING_PREF = false; + private UserManager mUserManager; private UserCapabilities mUserCaps; private boolean mGuestUserAutoCreated; @@ -297,6 +300,9 @@ public class UserDetailsSettings extends SettingsPreferenceFragment if (mGuestUserAutoCreated) { mRemoveUserPref.setEnabled((mUserInfo.flags & UserInfo.FLAG_INITIALIZED) != 0); } + if (!SHOW_APP_COPYING_PREF) { + removePreference(KEY_APP_COPYING); + } } else { mPhonePref.setChecked(!mUserManager.hasUserRestriction( UserManager.DISALLOW_OUTGOING_CALLS, new UserHandle(userId))); @@ -407,6 +413,9 @@ public class UserDetailsSettings extends SettingsPreferenceFragment } private void openAppCopyingScreen() { + if (!SHOW_APP_COPYING_PREF) { + return; + } final Bundle extras = new Bundle(); extras.putInt(AppRestrictionsFragment.EXTRA_USER_ID, mUserInfo.id); new SubSettingLauncher(getContext()) From d9281c887afca76f9f736eee911360b5ec01a1e0 Mon Sep 17 00:00:00 2001 From: Yasin Kilicdere Date: Thu, 13 Jan 2022 18:24:18 +0000 Subject: [PATCH 02/11] Make sure activity for adding a supervised user is opened full screen. Add supervised user activity only covers the right hand side of the screen in large screen devices. This CL makes sure it is always started from Settings with correct flags to be shown full-screen. Bug: 214401383 Bug: 205101183 Bug: 199868785 Test: croot && make RunSettingsRoboTests -j40 ROBOTEST_FILTER="com.android.settings.users.UserSettingsTest" Change-Id: If81e0cce91a5295eb2b93b12f68b70fd1240c953 --- src/com/android/settings/users/UserSettings.java | 3 ++- .../src/com/android/settings/users/UserSettingsTest.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 255b85fe0bf..3a43f269f49 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -503,7 +503,8 @@ public class UserSettings extends SettingsPreferenceFragment private void onAddSupervisedUserClicked() { final Intent intent = new Intent() .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) - .setPackage(mConfigSupervisedUserCreationPackage); + .setPackage(mConfigSupervisedUserCreationPackage) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // TODO(b/209659998): [to-be-removed] fallback activity for supervised user creation. if (getActivity().getPackageManager().resolveActivity(intent, 0) == null) { diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java index 5c13e2f8de6..2c694ace881 100644 --- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java +++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java @@ -664,6 +664,7 @@ public class UserSettingsTest { public void onPreferenceClick_addSupervisedUserClicked_startIntentWithAction() { final String intentPackage = "testPackage"; final String intentAction = UserManager.ACTION_CREATE_SUPERVISED_USER; + final int intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK; final int metricsAction = SettingsEnums.ACTION_USER_SUPERVISED_ADD; try { setConfigSupervisedUserCreationPackage(intentPackage); @@ -676,6 +677,7 @@ public class UserSettingsTest { verify(mFragment).startActivity(captor.capture()); assertThat(captor.getValue().getPackage()).isEqualTo(intentPackage); assertThat(captor.getValue().getAction()).isEqualTo(intentAction); + assertThat(captor.getValue().getFlags() & intentFlags).isGreaterThan(0); verify(mMetricsFeatureProvider).action(any(), eq(metricsAction)); } finally { From 33f2096fead29e5030d1da62523c1195085ad5a4 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Tue, 4 Jan 2022 18:08:28 +0800 Subject: [PATCH 03/11] [Panlingual] Adds a filter of application for per apps locale change. - Settings -> System -> app language -> app languages Use this filter to only show apps allowed to change the locale. - Settings -> Apps -> all apps -> any one of app -> language Use this filter to only show the language preference of app allowed to change the locale. Bug: 210935436 Test: atest pass. Test: local test. Change-Id: I2f8a0815dae68392e11882ad9e1e4945492efdba --- res/values/config.xml | 10 ++ .../settings/applications/AppLocaleUtil.java | 68 +++++++++ .../applications/AppStateLocaleBridge.java | 77 ++++++++++ .../AppLocalePreferenceController.java | 13 +- .../manageapplications/AppFilterRegistry.java | 17 ++- .../ManageApplications.java | 17 ++- .../applications/AppLocaleUtilTest.java | 135 ++++++++++++++++++ .../AppLocalePreferenceControllerTest.java | 37 ++++- 8 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 src/com/android/settings/applications/AppLocaleUtil.java create mode 100644 src/com/android/settings/applications/AppStateLocaleBridge.java create mode 100644 tests/unit/src/com/android/settings/applications/AppLocaleUtilTest.java diff --git a/res/values/config.xml b/res/values/config.xml index fe9f42d0e8c..56d3dca6956 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -560,4 +560,14 @@ false + + + + + + diff --git a/src/com/android/settings/applications/AppLocaleUtil.java b/src/com/android/settings/applications/AppLocaleUtil.java new file mode 100644 index 00000000000..e795b015792 --- /dev/null +++ b/src/com/android/settings/applications/AppLocaleUtil.java @@ -0,0 +1,68 @@ +/* + * 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.applications; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import com.android.settings.R; +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +/** This class provides methods that help dealing with per app locale. */ +public class AppLocaleUtil { + private static final String TAG = AppLocaleUtil.class.getSimpleName(); + + /** + * Decides the UI display of per app locale. + */ + public static boolean canDisplayLocaleUi(Context context, AppEntry app) { + return !isDisallowedPackage(context, app.info.packageName) + && !isSignedWithPlatformKey(context, app.info.packageName) + && app.hasLauncherEntry; + } + + private static boolean isDisallowedPackage(Context context, String packageName) { + final String[] disallowedPackages = context.getResources().getStringArray( + R.array.config_disallowed_app_localeChange_packages); + for (String disallowedPackage : disallowedPackages) { + if (packageName.equals(disallowedPackage)) { + return true; + } + } + return false; + } + + private static boolean isSignedWithPlatformKey(Context context, String packageName) { + PackageInfo packageInfo = null; + PackageManager packageManager = context.getPackageManager(); + ActivityManager activityManager = context.getSystemService(ActivityManager.class); + try { + packageInfo = packageManager.getPackageInfoAsUser( + packageName, /* flags= */ 0, + activityManager.getCurrentUser()); + } catch (PackageManager.NameNotFoundException ex) { + Log.e(TAG, "package not found: " + packageName); + } + if (packageInfo == null) { + return false; + } + return packageInfo.applicationInfo.isSignedWithPlatformKey(); + } +} diff --git a/src/com/android/settings/applications/AppStateLocaleBridge.java b/src/com/android/settings/applications/AppStateLocaleBridge.java new file mode 100644 index 00000000000..ebaf4abdea7 --- /dev/null +++ b/src/com/android/settings/applications/AppStateLocaleBridge.java @@ -0,0 +1,77 @@ +/* + * 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.applications; + +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; + +import java.util.List; + +/** + * Creates a application filter to restrict UI display of applications. + * This is to avoid users from changing the per apps locale + * Also provides app filters that can use the info. + */ +public class AppStateLocaleBridge extends AppStateBaseBridge { + private static final String TAG = AppStateLocaleBridge.class.getSimpleName(); + + private final Context mContext; + + public AppStateLocaleBridge(Context context, ApplicationsState appState, + Callback callback) { + super(appState, callback); + mContext = context; + } + + @Override + protected void updateExtraInfo(AppEntry app, String packageName, int uid) { + app.extraInfo = AppLocaleUtil.canDisplayLocaleUi(mContext, app) + ? Boolean.TRUE : Boolean.FALSE; + } + + @Override + protected void loadAllExtraInfo() { + final List allApps = mAppSession.getAllApps(); + for (int i = 0; i < allApps.size(); i++) { + AppEntry app = allApps.get(i); + app.extraInfo = AppLocaleUtil.canDisplayLocaleUi(mContext, app) + ? Boolean.TRUE : Boolean.FALSE; + } + } + + /** For the Settings which shows category of per app's locale. */ + public static final AppFilter FILTER_APPS_LOCALE = + new AppFilter() { + @Override + public void init() { + } + + @Override + public boolean filterApp(AppEntry entry) { + if (entry.extraInfo == null) { + Log.d(TAG, "No extra info."); + return false; + } + return (Boolean) entry.extraInfo; + } + }; + + +} diff --git a/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java b/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java index f1e43ad1ee9..810d2304cbe 100644 --- a/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppLocalePreferenceController.java @@ -20,20 +20,23 @@ import android.content.Context; import android.util.FeatureFlagUtils; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.applications.AppLocaleUtil; /** * A controller to update current locale information of application. */ public class AppLocalePreferenceController extends AppInfoPreferenceControllerBase { + private static final String TAG = AppLocalePreferenceController.class.getSimpleName(); + public AppLocalePreferenceController(Context context, String key) { super(context, key); } @Override public int getAvailabilityStatus() { - return FeatureFlagUtils - .isEnabled(mContext, FeatureFlagUtils.SETTINGS_APP_LANGUAGE_SELECTION) - ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + boolean isFeatureOn = FeatureFlagUtils + .isEnabled(mContext, FeatureFlagUtils.SETTINGS_APP_LANGUAGE_SELECTION); + return isFeatureOn && canDisplayLocaleUi() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @Override @@ -45,4 +48,8 @@ public class AppLocalePreferenceController extends AppInfoPreferenceControllerBa public CharSequence getSummary() { return AppLocaleDetails.getSummary(mContext, mParent.getAppEntry().info.packageName); } + + boolean canDisplayLocaleUi() { + return AppLocaleUtil.canDisplayLocaleUi(mContext, mParent.getAppEntry()); + } } diff --git a/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java b/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java index d1d4f622cc8..6e67815d7e7 100644 --- a/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java +++ b/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java @@ -21,6 +21,7 @@ import androidx.annotation.IntDef; import com.android.settings.R; import com.android.settings.applications.AppStateAlarmsAndRemindersBridge; import com.android.settings.applications.AppStateInstallAppsBridge; +import com.android.settings.applications.AppStateLocaleBridge; import com.android.settings.applications.AppStateManageExternalStorageBridge; import com.android.settings.applications.AppStateMediaManagementAppsBridge; import com.android.settings.applications.AppStateNotificationBridge; @@ -54,6 +55,7 @@ public class AppFilterRegistry { FILTER_APPS_BLOCKED, FILTER_ALARMS_AND_REMINDERS, FILTER_APPS_MEDIA_MANAGEMENT, + FILTER_APPS_LOCALE, }) @interface FilterType { } @@ -79,14 +81,15 @@ public class AppFilterRegistry { public static final int FILTER_MANAGE_EXTERNAL_STORAGE = 17; public static final int FILTER_ALARMS_AND_REMINDERS = 18; public static final int FILTER_APPS_MEDIA_MANAGEMENT = 19; - // Next id: 20. If you add an entry here, length of mFilters should be updated + public static final int FILTER_APPS_LOCALE = 20; + // Next id: 21. If you add an entry here, length of mFilters should be updated private static AppFilterRegistry sRegistry; private final AppFilterItem[] mFilters; private AppFilterRegistry() { - mFilters = new AppFilterItem[20]; + mFilters = new AppFilterItem[21]; // High power allowlist, on mFilters[FILTER_APPS_POWER_ALLOWLIST] = new AppFilterItem( @@ -203,8 +206,16 @@ public class AppFilterRegistry { AppStateMediaManagementAppsBridge.FILTER_MEDIA_MANAGEMENT_APPS, FILTER_APPS_MEDIA_MANAGEMENT, R.string.media_management_apps_title); + + // Apps that can configurate appication's locale. + mFilters[FILTER_APPS_LOCALE] = new AppFilterItem( + AppStateLocaleBridge.FILTER_APPS_LOCALE, + FILTER_APPS_LOCALE, + R.string.app_locale_picker_title); } + + public static AppFilterRegistry getInstance() { if (sRegistry == null) { sRegistry = new AppFilterRegistry(); @@ -235,6 +246,8 @@ public class AppFilterRegistry { return FILTER_ALARMS_AND_REMINDERS; case ManageApplications.LIST_TYPE_MEDIA_MANAGEMENT_APPS: return FILTER_APPS_MEDIA_MANAGEMENT; + case ManageApplications.LIST_TYPE_APPS_LOCALE: + return FILTER_APPS_LOCALE; default: return FILTER_APPS_ALL; } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index d98548280fb..01bc2f19bb9 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -95,6 +95,7 @@ import com.android.settings.applications.AppStateAlarmsAndRemindersBridge; import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; import com.android.settings.applications.AppStateBaseBridge; import com.android.settings.applications.AppStateInstallAppsBridge; +import com.android.settings.applications.AppStateLocaleBridge; import com.android.settings.applications.AppStateManageExternalStorageBridge; import com.android.settings.applications.AppStateMediaManagementAppsBridge; import com.android.settings.applications.AppStateNotificationBridge; @@ -232,7 +233,7 @@ public class ManageApplications extends InstrumentedFragment public static final int LIST_MANAGE_EXTERNAL_STORAGE = 11; public static final int LIST_TYPE_ALARMS_AND_REMINDERS = 12; public static final int LIST_TYPE_MEDIA_MANAGEMENT_APPS = 13; - public static final int LIST_TYPE_APPS_LOCAL = 14; + public static final int LIST_TYPE_APPS_LOCALE = 14; // List types that should show instant apps. public static final Set LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList( @@ -321,7 +322,7 @@ public class ManageApplications extends InstrumentedFragment mNotificationBackend = new NotificationBackend(); mSortOrder = R.id.sort_order_recent_notification; } else if (className.equals(AppLocaleDetails.class.getName())) { - mListType = LIST_TYPE_APPS_LOCAL; + mListType = LIST_TYPE_APPS_LOCALE; } else { mListType = LIST_TYPE_MAIN; } @@ -504,7 +505,7 @@ public class ManageApplications extends InstrumentedFragment return SettingsEnums.ALARMS_AND_REMINDERS; case LIST_TYPE_MEDIA_MANAGEMENT_APPS: return SettingsEnums.MEDIA_MANAGEMENT_APPS; - case LIST_TYPE_APPS_LOCAL: + case LIST_TYPE_APPS_LOCALE: return SettingsEnums.APPS_LOCALE_LIST; default: return SettingsEnums.PAGE_UNKNOWN; @@ -629,7 +630,7 @@ public class ManageApplications extends InstrumentedFragment startAppInfoFragment(MediaManagementAppsDetails.class, R.string.media_management_apps_title); break; - case LIST_TYPE_APPS_LOCAL: + case LIST_TYPE_APPS_LOCALE: startAppInfoFragment(AppLocaleDetails.class, R.string.app_locale_picker_title); break; @@ -743,9 +744,9 @@ public class ManageApplications extends InstrumentedFragment && mSortOrder != R.id.sort_order_size); mOptionsMenu.findItem(R.id.show_system).setVisible(!mShowSystem - && mListType != LIST_TYPE_HIGH_POWER); + && mListType != LIST_TYPE_HIGH_POWER && mListType != LIST_TYPE_APPS_LOCALE); mOptionsMenu.findItem(R.id.hide_system).setVisible(mShowSystem - && mListType != LIST_TYPE_HIGH_POWER); + && mListType != LIST_TYPE_HIGH_POWER && mListType != LIST_TYPE_APPS_LOCALE); mOptionsMenu.findItem(R.id.reset_app_preferences).setVisible(mListType == LIST_TYPE_MAIN); @@ -1100,6 +1101,8 @@ public class ManageApplications extends InstrumentedFragment mExtraInfoBridge = new AppStateAlarmsAndRemindersBridge(mContext, mState, this); } else if (mManageApplications.mListType == LIST_TYPE_MEDIA_MANAGEMENT_APPS) { mExtraInfoBridge = new AppStateMediaManagementAppsBridge(mContext, mState, this); + } else if (mManageApplications.mListType == LIST_TYPE_APPS_LOCALE) { + mExtraInfoBridge = new AppStateLocaleBridge(mContext, mState, this); } else { mExtraInfoBridge = null; } @@ -1533,7 +1536,7 @@ public class ManageApplications extends InstrumentedFragment case LIST_TYPE_MEDIA_MANAGEMENT_APPS: holder.setSummary(MediaManagementAppsDetails.getSummary(mContext, entry)); break; - case LIST_TYPE_APPS_LOCAL: + case LIST_TYPE_APPS_LOCALE: holder.setSummary(AppLocaleDetails .getSummary(mContext, entry.info.packageName)); break; diff --git a/tests/unit/src/com/android/settings/applications/AppLocaleUtilTest.java b/tests/unit/src/com/android/settings/applications/AppLocaleUtilTest.java new file mode 100644 index 00000000000..22a055f34da --- /dev/null +++ b/tests/unit/src/com/android/settings/applications/AppLocaleUtilTest.java @@ -0,0 +1,135 @@ +/* + * 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.applications; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class AppLocaleUtilTest { + @Mock + private PackageManager mPackageManager; + @Mock + private ActivityManager mActivityManager; + @Mock + private AppEntry mEntry; + @Mock + private ApplicationInfo mApplicationInfo; + @Mock + private Resources mResources; + + private Context mContext; + private String mDisallowedPackage = "com.disallowed.package"; + private String mAallowedPackage = "com.allowed.package"; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager); + } + + @Test + public void isDisplayLocaleUi_showUI() throws PackageManager.NameNotFoundException { + setTestAppEntry(mAallowedPackage); + setDisallowedPackageName(mDisallowedPackage); + setApplicationInfo(/*no platform key*/false); + mEntry.hasLauncherEntry = true; + + assertTrue(AppLocaleUtil.canDisplayLocaleUi(mContext, mEntry)); + } + + @Test + public void isDisplayLocaleUi_notShowUI_hasPlatformKey() + throws PackageManager.NameNotFoundException { + setTestAppEntry(mAallowedPackage); + setDisallowedPackageName(mDisallowedPackage); + setApplicationInfo(/*has platform key*/true); + mEntry.hasLauncherEntry = true; + + assertFalse(AppLocaleUtil.canDisplayLocaleUi(mContext, mEntry)); + } + + @Test + public void isDisplayLocaleUi_notShowUI_noLauncherEntry() + throws PackageManager.NameNotFoundException { + setTestAppEntry(mAallowedPackage); + setDisallowedPackageName(mDisallowedPackage); + setApplicationInfo(/*no platform key*/false); + mEntry.hasLauncherEntry = false; + + assertFalse(AppLocaleUtil.canDisplayLocaleUi(mContext, mEntry)); + } + + @Test + public void isDisplayLocaleUi_notShowUI_matchDisallowedPackageList() + throws PackageManager.NameNotFoundException { + setTestAppEntry(mDisallowedPackage); + setDisallowedPackageName(mDisallowedPackage); + setApplicationInfo(/*no platform key*/false); + mEntry.hasLauncherEntry = false; + + assertFalse(AppLocaleUtil.canDisplayLocaleUi(mContext, mEntry)); + } + + private void setTestAppEntry(String packageName) { + mEntry.info = mApplicationInfo; + mApplicationInfo.packageName = packageName; + } + + private void setDisallowedPackageName(String packageName) { + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getStringArray(anyInt())).thenReturn(new String[]{packageName}); + } + + private void setApplicationInfo(boolean signedWithPlatformKey) + throws PackageManager.NameNotFoundException { + ApplicationInfo applicationInfo = new ApplicationInfo(); + if (signedWithPlatformKey) { + applicationInfo.privateFlags = applicationInfo.privateFlags + | ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY; + } + + PackageInfo packageInfo = new PackageInfo(); + packageInfo.applicationInfo = applicationInfo; + when(mPackageManager.getPackageInfoAsUser(anyString(), anyInt(), anyInt())).thenReturn( + packageInfo); + } +} diff --git a/tests/unit/src/com/android/settings/applications/appinfo/AppLocalePreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/appinfo/AppLocalePreferenceControllerTest.java index d7e3f923297..526b6cc9e3e 100644 --- a/tests/unit/src/com/android/settings/applications/appinfo/AppLocalePreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/applications/appinfo/AppLocalePreferenceControllerTest.java @@ -18,8 +18,6 @@ package com.android.settings.applications.appinfo; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.spy; - import android.content.Context; import android.util.FeatureFlagUtils; @@ -37,20 +35,27 @@ import org.mockito.MockitoAnnotations; public class AppLocalePreferenceControllerTest { private Context mContext; + private boolean mCanDisplayLocaleUi; private AppLocalePreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = spy(ApplicationProvider.getApplicationContext()); + mContext = ApplicationProvider.getApplicationContext(); - mController = spy(new AppLocalePreferenceController(mContext, "test_key")); + mController = new AppLocalePreferenceController(mContext, "test_key") { + @Override + boolean canDisplayLocaleUi() { + return mCanDisplayLocaleUi; + } + }; FeatureFlagUtils .setEnabled(mContext, FeatureFlagUtils.SETTINGS_APP_LANGUAGE_SELECTION, true); } @Test - public void getAvailabilityStatus_featureFlagOff_shouldReturnUnavailable() { + public void getAvailabilityStatus_canShowUiButFeatureFlagOff_shouldReturnUnavailable() { + mCanDisplayLocaleUi = true; FeatureFlagUtils .setEnabled(mContext, FeatureFlagUtils.SETTINGS_APP_LANGUAGE_SELECTION, false); @@ -59,8 +64,28 @@ public class AppLocalePreferenceControllerTest { } @Test - public void getAvailabilityStatus_featureFlagOn_shouldReturnAvailable() { + public void getAvailabilityStatus_canShowUiAndFeatureFlagOn_shouldReturnAvailable() { + mCanDisplayLocaleUi = true; + assertThat(mController.getAvailabilityStatus()) .isEqualTo(BasePreferenceController.AVAILABLE); } + + @Test + public void getAvailabilityStatus_featureFlagOnButCanNotShowUi_shouldReturnUnavailable() { + mCanDisplayLocaleUi = false; + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_featureFlagOffAndCanNotShowUi_shouldReturnUnavailable() { + mCanDisplayLocaleUi = false; + FeatureFlagUtils + .setEnabled(mContext, FeatureFlagUtils.SETTINGS_APP_LANGUAGE_SELECTION, false); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + } } From 714bcfff2c533344b0a56b2b31f02a5d7697831a Mon Sep 17 00:00:00 2001 From: danielwbhuang Date: Thu, 20 Jan 2022 22:46:44 +0800 Subject: [PATCH 04/11] [Panlingual] Suggested locales update 1. Extract the Country from the Top system langauge, TelephonyManager.getSimCountryIso().toUpperCase(Locale.US), and TelephonyManager.getNetworkCountryIso().toUpperCase(Locale.US). 2. For each app-supported locale, if the region is identical then add this as a suggested language. Bug: 209729740 Test: atest AppLocaleDetailsTest Change-Id: I4f95a638916e88197fc37546c7786c63b59f581d --- .../appinfo/AppLocaleDetails.java | 40 +++++++-- .../appinfo/AppLocaleDetailsTest.java | 84 ++++++++++++------- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java index 7fd43c22e49..aee360e22f6 100644 --- a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java +++ b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java @@ -37,6 +37,8 @@ import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; +import com.android.internal.app.LocalePicker; +import com.android.internal.app.LocalePicker.LocaleInfo; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; @@ -45,8 +47,11 @@ import com.android.settingslib.applications.AppUtils; import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.widget.RadioButtonPreference; +import com.google.common.collect.Iterables; + import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Locale; /** @@ -200,8 +205,8 @@ public class AppLocaleDetails extends AppInfoBase implements RadioButtonPreferen private TelephonyManager mTelephonyManager; private LocaleManager mLocaleManager; - private Collection mSuggestedLocales = new ArrayList<>();; - private Collection mSupportedLocales = new ArrayList<>();; + private Collection mSuggestedLocales = new ArrayList<>(); + private Collection mSupportedLocales = new ArrayList<>(); AppLocaleDetailsHelper(Context context, String packageName) { mContext = context; @@ -230,25 +235,41 @@ public class AppLocaleDetails extends AppInfoBase implements RadioButtonPreferen @VisibleForTesting void handleSuggestedLocales() { LocaleList currentSystemLocales = getCurrentSystemLocales(); - Locale simLocale = mTelephonyManager.getSimLocale(); Locale appLocale = getAppDefaultLocale(mContext, mPackageName); + String simCountry = mTelephonyManager.getSimCountryIso().toUpperCase(Locale.US); + String networkCountry = mTelephonyManager.getNetworkCountryIso().toUpperCase(Locale.US); // 1st locale in suggested languages group. if (appLocale != null) { mSuggestedLocales.add(appLocale); } // 2nd locale in suggested languages group. - if (simLocale != null && !compareLocale(simLocale, appLocale)) { - mSuggestedLocales.add(simLocale); + final List localeInfos = LocalePicker.getAllAssetLocales(mContext, false); + for (LocaleInfo localeInfo : localeInfos) { + Locale locale = localeInfo.getLocale(); + String localeCountry = locale.getCountry().toUpperCase(Locale.US); + if (!compareLocale(locale, appLocale) + && isCountrySuggestedLocale(localeCountry, simCountry, networkCountry)) { + mSuggestedLocales.add(locale); + } } // Other locales in suggested languages group. for (int i = 0; i < currentSystemLocales.size(); i++) { Locale locale = currentSystemLocales.get(i); - if (!compareLocale(locale, appLocale) && !compareLocale(locale, simLocale)) { + boolean isInSuggestedLocales = false; + for (int j = 0; j < mSuggestedLocales.size(); j++) { + Locale suggestedLocale = Iterables.get(mSuggestedLocales, j); + if (compareLocale(locale, suggestedLocale)) { + isInSuggestedLocales = true; + break; + } + } + if (!isInSuggestedLocales) { mSuggestedLocales.add(locale); } } } + @VisibleForTesting static boolean compareLocale(Locale source, Locale target) { if (source == null && target == null) { return true; @@ -259,6 +280,13 @@ public class AppLocaleDetails extends AppInfoBase implements RadioButtonPreferen } } + private static boolean isCountrySuggestedLocale(String localeCountry, + String simCountry, + String networkCountry) { + return ((!simCountry.isEmpty() && simCountry.equals(localeCountry)) + || (!networkCountry.isEmpty() && networkCountry.equals(localeCountry))); + } + @VisibleForTesting void handleSupportedLocales() { //TODO Waiting for PackageManager api diff --git a/tests/unit/src/com/android/settings/applications/appinfo/AppLocaleDetailsTest.java b/tests/unit/src/com/android/settings/applications/appinfo/AppLocaleDetailsTest.java index e185354e5ed..1042a6a20a7 100644 --- a/tests/unit/src/com/android/settings/applications/appinfo/AppLocaleDetailsTest.java +++ b/tests/unit/src/com/android/settings/applications/appinfo/AppLocaleDetailsTest.java @@ -53,7 +53,6 @@ public class AppLocaleDetailsTest { private Context mContext; private LocaleList mSystemLocales; - private Locale mSimLocale; private LocaleList mAppLocale; private String[] mAssetLocales; @@ -69,7 +68,8 @@ public class AppLocaleDetailsTest { when(mContext.getSystemService(LocaleManager.class)).thenReturn(mLocaleManager); setupInitialLocales("en", - "uk", + "tw", + "jp", "en, uk, jp, ne", new String[]{"en", "ne", "ms", "pa"}); } @@ -88,6 +88,8 @@ public class AppLocaleDetailsTest { @Test @UiThreadTest public void handleAllLocalesData_1stLocaleOfSuggestedLocaleListIsAppLocale() { + Locale simCountryLocale = new Locale("zh", "TW"); + Locale networkCountryLocale = new Locale("ja", "JP"); DummyAppLocaleDetailsHelper helper = new DummyAppLocaleDetailsHelper(mContext, APP_PACKAGE_NAME); @@ -95,25 +97,17 @@ public class AppLocaleDetailsTest { Locale locale = Iterables.get(helper.getSuggestedLocales(), 0); assertTrue(locale.equals(mAppLocale.get(0))); + assertTrue(helper.getSuggestedLocales().contains(simCountryLocale)); + assertTrue(helper.getSuggestedLocales().contains(networkCountryLocale)); } @Test @UiThreadTest - public void handleAllLocalesData_2ndLocaleOfSuggestedLocaleListIsSimLocale() { - DummyAppLocaleDetailsHelper helper = - new DummyAppLocaleDetailsHelper(mContext, APP_PACKAGE_NAME); - - helper.handleAllLocalesData(); - - Locale locale = Iterables.get(helper.getSuggestedLocales(), 1); - assertTrue(locale.equals(mSimLocale)); - } - - @Test - @UiThreadTest - public void handleAllLocalesData_withoutAppLocale_1stLocaleOfSuggestedLocaleListIsSimLocal() { + public void handleAllLocalesData_withoutAppLocale_1stSuggestedLocaleIsSimCountryLocale() { + Locale simCountryLocale = new Locale("zh", "TW"); setupInitialLocales("", - "uk", + "tw", + "", "en, uk, jp, ne", new String[]{"en", "ne", "ms", "pa"}); DummyAppLocaleDetailsHelper helper = @@ -122,13 +116,34 @@ public class AppLocaleDetailsTest { helper.handleAllLocalesData(); Locale locale = Iterables.get(helper.getSuggestedLocales(), 0); - assertTrue(locale.equals(mSimLocale)); + assertTrue(locale.equals(simCountryLocale)); + assertFalse(helper.getSuggestedLocales().contains(mAppLocale.get(0))); } @Test @UiThreadTest - public void handleAllLocalesData_noAppAndSimLocale_1stLocaleIsFirstOneInSystemLocales() { + public void handleAllLocalesData_withoutAppLocale_1stSuggestedLocaleIsNetworkCountryLocale() { + Locale networkCountryLocale = new Locale("en", "GB"); setupInitialLocales("", + "", + "gb", + "en, uk, jp, ne", + new String[]{"en", "ne", "ms", "pa"}); + DummyAppLocaleDetailsHelper helper = + new DummyAppLocaleDetailsHelper(mContext, APP_PACKAGE_NAME); + + helper.handleAllLocalesData(); + + Locale locale = Iterables.get(helper.getSuggestedLocales(), 0); + assertTrue(locale.equals(networkCountryLocale)); + assertFalse(helper.getSuggestedLocales().contains(mAppLocale.get(0))); + } + + @Test + @UiThreadTest + public void handleAllLocalesData_noAppAndSimNetworkLocale_1stLocaleIsFirstOneInSystemLocales() { + setupInitialLocales("", + "", "", "en, uk, jp, ne", new String[]{"en", "ne", "ms", "pa"}); @@ -175,25 +190,34 @@ public class AppLocaleDetailsTest { /** * Sets the initial Locale data * - * @param appLocale Application locale, it shall be a language tag. - * example: "en" - * @param simLocale SIM carrier locale, it shall be a language tag. - * example: "en" - * @param systemLocales System locales, a locale list by a multiple language tags with comma. - * example: "en, uk, jp" - * @param assetLocales Asset locales, a locale list by a multiple language tags with String - * array. - * example: new String[] {"en", "ne", "ms", "pa"} + * @param appLocale Application locale, it shall be a language tag. + * example: "en" + * + * @param simCountry The ISO-3166-1 alpha-2 country code equivalent for the SIM + * provider's country code. + * example: "us" + * + * @param networkCountry The ISO-3166-1 alpha-2 country code equivalent of the MCC + * (Mobile Country Code) of the current registered operato + * or the cell nearby. + * example: "us" + * + * @param systemLocales System locales, a locale list by a multiple language tags with comma. + * example: "en, uk, jp" + * @param assetLocales Asset locales, a locale list by a multiple language tags with String + * array. + * example: new String[] {"en", "ne", "ms", "pa"} */ private void setupInitialLocales(String appLocale, - String simLocale, + String simCountry, + String networkCountry, String systemLocales, String[] assetLocales) { mAppLocale = LocaleList.forLanguageTags(appLocale); - mSimLocale = Locale.forLanguageTag(simLocale); mSystemLocales = LocaleList.forLanguageTags(systemLocales); mAssetLocales = assetLocales; - when(mTelephonyManager.getSimLocale()).thenReturn(simLocale.isEmpty() ? null : mSimLocale); + when(mTelephonyManager.getSimCountryIso()).thenReturn(simCountry); + when(mTelephonyManager.getNetworkCountryIso()).thenReturn(networkCountry); when(mLocaleManager.getApplicationLocales(anyString())).thenReturn(mAppLocale); } From a0dc31f3c53cde9cc5e23d8b6e3a6c2af63fbcf2 Mon Sep 17 00:00:00 2001 From: Michael Groover Date: Thu, 20 Jan 2022 11:40:48 -0800 Subject: [PATCH 05/11] Resolve test failure in WorkModePreferenceControllerTest Android T introduced a new requirement that apps registering for non-system broadcasts via Context#registerReceiver must specify a flag indicating whether the receiver should be exported. This flag was added to the receiver in WorkModePreferenceController, but this broke a test since the signature of the registerReceiver method was changed. This commit updates the signature of the method in the test to also expect an int parameter for the flags. Fixes: 214906107 Test: make RunSettingsRoboTests ROBOTEST_FILTER=WorkModePreferenceControllerTest Change-Id: I17061ab24ed3ec3def4062fe461803ad88bce9c8 --- .../settings/accounts/WorkModePreferenceControllerTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java index c7e571fafd2..b5d1cc78eba 100644 --- a/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java @@ -18,6 +18,7 @@ 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.verify; @@ -32,7 +33,6 @@ import androidx.preference.SwitchPreference; import com.android.settings.R; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -106,10 +106,9 @@ public class WorkModePreferenceControllerTest { } @Test - @Ignore public void onStart_shouldRegisterReceiver() { mController.onStart(); - verify(mContext).registerReceiver(eq(mController.mReceiver), any()); + verify(mContext).registerReceiver(eq(mController.mReceiver), any(), anyInt()); } @Test From 14e6c7368781e9f28e902b9b76110a357758901c Mon Sep 17 00:00:00 2001 From: Kate Montgomery Date: Wed, 19 Jan 2022 18:15:28 +0000 Subject: [PATCH 06/11] Make the show/hide system setting for recent location acccesses sticky. This is in order to align the location indicator with the list of recent accesses shown in Settings. Bug: 191503437 Test: manual Change-Id: I7aaa65390567ca644e7137de15c558cdcdb29935 --- ...entLocationAccessPreferenceController.java | 11 ++++++++-- .../RecentLocationAccessSeeAllFragment.java | 18 +++++---------- ...ationAccessSeeAllPreferenceController.java | 3 +++ ...ntLocationRequestPreferenceController.java | 6 ++++- ...ocationAccessPreferenceControllerTest.java | 22 +++++++++++++++++++ ...cationRequestPreferenceControllerTest.java | 20 +++++++++++++++++ 6 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java index a8a30b46c93..ba660ee3c6d 100644 --- a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.icu.text.RelativeDateTimeFormatter; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -85,11 +86,17 @@ public class RecentLocationAccessPreferenceController extends LocationBasePrefer public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mCategoryRecentLocationRequests = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + mCategoryRecentLocationRequests.removeAll(); final Context prefContext = mCategoryRecentLocationRequests.getContext(); final List recentLocationAccesses = new ArrayList<>(); final UserManager userManager = UserManager.get(mContext); - for (RecentAppOpsAccess.Access access : mRecentLocationApps.getAppListSorted( - /* showSystemApps= */ false)) { + final boolean showSystem = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0) == 1; + for (RecentAppOpsAccess.Access access : mRecentLocationApps.getAppListSorted(showSystem)) { if (isRequestMatchesProfileType(userManager, access, mType)) { recentLocationAccesses.add(access); if (recentLocationAccesses.size() == MAX_APPS) { diff --git a/src/com/android/settings/location/RecentLocationAccessSeeAllFragment.java b/src/com/android/settings/location/RecentLocationAccessSeeAllFragment.java index e27b28c8238..f7bf31a896a 100644 --- a/src/com/android/settings/location/RecentLocationAccessSeeAllFragment.java +++ b/src/com/android/settings/location/RecentLocationAccessSeeAllFragment.java @@ -17,6 +17,7 @@ package com.android.settings.location; import android.content.Context; import android.os.Bundle; +import android.provider.Settings; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -36,7 +37,6 @@ public class RecentLocationAccessSeeAllFragment extends DashboardFragment { private static final int MENU_SHOW_SYSTEM = Menu.FIRST + 1; private static final int MENU_HIDE_SYSTEM = Menu.FIRST + 2; - private static final String EXTRA_SHOW_SYSTEM = "show_system"; private boolean mShowSystem = false; private MenuItem mShowSystemMenu; @@ -58,18 +58,8 @@ public class RecentLocationAccessSeeAllFragment extends DashboardFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mShowSystem = savedInstanceState.getBoolean(EXTRA_SHOW_SYSTEM, mShowSystem); - } - if (mController != null) { - mController.setShowSystem(mShowSystem); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(EXTRA_SHOW_SYSTEM, mShowSystem); + mShowSystem = Settings.Secure.getInt(getContentResolver(), + Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0) == 1; } @Override @@ -88,6 +78,8 @@ public class RecentLocationAccessSeeAllFragment extends DashboardFragment { case MENU_SHOW_SYSTEM: case MENU_HIDE_SYSTEM: mShowSystem = menuItem.getItemId() == MENU_SHOW_SYSTEM; + Settings.Secure.putInt(getContentResolver(), + Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, mShowSystem ? 1 : 0); updateMenu(); if (mController != null) { mController.setShowSystem(mShowSystem); diff --git a/src/com/android/settings/location/RecentLocationAccessSeeAllPreferenceController.java b/src/com/android/settings/location/RecentLocationAccessSeeAllPreferenceController.java index bca4486f01c..e3379c7da7f 100644 --- a/src/com/android/settings/location/RecentLocationAccessSeeAllPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationAccessSeeAllPreferenceController.java @@ -20,6 +20,7 @@ import static com.android.settings.location.RecentLocationAccessPreferenceContro import android.content.Context; import android.os.UserManager; +import android.provider.Settings; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -43,6 +44,8 @@ public class RecentLocationAccessSeeAllPreferenceController public RecentLocationAccessSeeAllPreferenceController(Context context, String key) { super(context, key); + mShowSystem = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0) == 1; mRecentLocationAccesses = RecentAppOpsAccess.createForLocation(context); } diff --git a/src/com/android/settings/location/RecentLocationRequestPreferenceController.java b/src/com/android/settings/location/RecentLocationRequestPreferenceController.java index 812a4404fb1..a14e047cb4b 100644 --- a/src/com/android/settings/location/RecentLocationRequestPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationRequestPreferenceController.java @@ -17,6 +17,7 @@ import android.content.Context; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -83,8 +84,11 @@ public class RecentLocationRequestPreferenceController extends LocationBasePrefe final Context prefContext = mCategoryRecentLocationRequests.getContext(); final List recentLocationRequests = new ArrayList<>(); final UserManager userManager = UserManager.get(mContext); + final boolean showSystem = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 0) == 1; + for (RecentLocationApps.Request request : mRecentLocationApps.getAppListSorted( - false /* systemApps */)) { + showSystem)) { if (isRequestMatchesProfileType(userManager, request, mType)) { recentLocationRequests.add(request); if (recentLocationRequests.size() == MAX_APPS) { diff --git a/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java index 52068c4d7c3..225f91bde17 100644 --- a/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java @@ -19,9 +19,12 @@ import static com.google.common.truth.Truth.assertThat; 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.Context; +import android.os.UserHandle; +import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; @@ -34,12 +37,15 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.testutils.shadow.ShadowDeviceConfig; import com.android.settingslib.applications.RecentAppOpsAccess; +import com.google.common.collect.ImmutableList; + import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @@ -107,4 +113,20 @@ public class RecentLocationAccessPreferenceControllerTest { mContext.getText(R.string.location_recent_location_access_view_details)); assertThat(details.hasOnClickListeners()).isTrue(); } + + /** Verifies the title text, details text are correct, and the click listener is set. */ + @Test + public void updateState_showSystemAccess() { + doReturn(ImmutableList.of( + new RecentAppOpsAccess.Access("app", UserHandle.CURRENT, null, "app", "", 0))) + .when(mRecentLocationApps).getAppListSorted(false); + doReturn(new ArrayList<>()).when(mRecentLocationApps).getAppListSorted(true); + mController.displayPreference(mScreen); + mController.updateState(mLayoutPreference); + verify(mLayoutPreference).addPreference(Mockito.any()); + + Settings.Secure.putInt( + mContext.getContentResolver(), Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 1); + verify(mLayoutPreference, Mockito.times(1)).addPreference(Mockito.any()); + } } diff --git a/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java index 545a3584e6e..be778cbf29f 100644 --- a/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.UserHandle; +import android.provider.Settings; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; @@ -81,6 +82,25 @@ public class RecentLocationRequestPreferenceControllerTest { verify(mCategory, times(3)).addPreference(any()); } + @Test + public void updateState_whenAppListMoreThanThree_showSystem() { + when(mController.mRecentLocationApps.getAppListSorted(false)) + .thenReturn(createMockRequest(2)); + when(mController.mRecentLocationApps.getAppListSorted(true)) + .thenReturn(createMockRequest(3)); + + mController.displayPreference(mScreen); + verify(mCategory, times(2)).addPreference(any()); + + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, + 1); + + mController.displayPreference(mScreen); + verify(mCategory, times(5)).addPreference(any()); + } + @Test public void updateState_workProfile_shouldShowOnlyWorkProfileApps() { final List requests = createMockRequest(6); From 885dff9b9ac9f4a7e968521fc397442273b10abc Mon Sep 17 00:00:00 2001 From: Lifu Tang Date: Thu, 20 Jan 2022 14:23:16 -0800 Subject: [PATCH 07/11] Fix broken test Bug: 215047319 Test: run test case `LocationInjectedServicesPreferenceControllerTest` Change-Id: I446f1341042d69ea6657d2f4bf1fbbac48961a4f --- .../LocationInjectedServicesPreferenceControllerTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/robotests/src/com/android/settings/location/LocationInjectedServicesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/LocationInjectedServicesPreferenceControllerTest.java index ad928da6218..ae62724cea5 100644 --- a/tests/robotests/src/com/android/settings/location/LocationInjectedServicesPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/location/LocationInjectedServicesPreferenceControllerTest.java @@ -43,7 +43,6 @@ import com.android.settings.widget.RestrictedAppPreference; import com.android.settingslib.core.lifecycle.Lifecycle; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -98,16 +97,15 @@ public class LocationInjectedServicesPreferenceControllerTest { } @Test - @Ignore public void onResume_shouldRegisterListener() { mController.onResume(); verify(mContext).registerReceiver(eq(mController.mInjectedSettingsReceiver), - eq(mController.INTENT_FILTER_INJECTED_SETTING_CHANGED)); + eq(mController.INTENT_FILTER_INJECTED_SETTING_CHANGED), + anyInt()); } @Test - @Ignore public void onPause_shouldUnregisterListener() { mController.onResume(); mController.onPause(); From 781ea4c6ab6e74b3d8a0be7c051ccccf68571e3e Mon Sep 17 00:00:00 2001 From: Weng Su Date: Fri, 21 Jan 2022 09:41:01 +0000 Subject: [PATCH 08/11] Mock WifiP2pManager.class for ResetNetworkConfirmTest Bug: 214906101 Test: manual test make RunSettingsRoboTests ROBOTEST_FILTER=ResetNetworkConfirmTest Change-Id: I3aa22c06fc8fcd22deb8b15b2998539b48d3ce39 --- .../settings/ResetNetworkConfirmTest.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java b/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java index e7a0090a53a..e4495f42b90 100644 --- a/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java +++ b/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java @@ -18,6 +18,15 @@ package com.android.settings; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +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.wifi.p2p.WifiP2pManager; +import android.os.Looper; import android.view.LayoutInflater; import android.widget.TextView; @@ -25,7 +34,6 @@ import androidx.fragment.app.FragmentActivity; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowRecoverySystem; -import com.android.settings.testutils.shadow.ShadowWifiP2pManager; import org.junit.After; import org.junit.Before; @@ -39,27 +47,31 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowRecoverySystem.class, - ShadowWifiP2pManager.class, ShadowBluetoothAdapter.class -}) +@Config(shadows = {ShadowRecoverySystem.class, ShadowBluetoothAdapter.class}) public class ResetNetworkConfirmTest { private FragmentActivity mActivity; + + @Mock + private WifiP2pManager mWifiP2pManager; @Mock private ResetNetworkConfirm mResetNetworkConfirm; @Before public void setUp() { MockitoAnnotations.initMocks(this); + when(mWifiP2pManager.initialize(any(Context.class), any(Looper.class), any())) + .thenReturn(mock(WifiP2pManager.Channel.class)); + mResetNetworkConfirm = new ResetNetworkConfirm(); - mActivity = Robolectric.setupActivity(FragmentActivity.class); + mActivity = spy(Robolectric.setupActivity(FragmentActivity.class)); + when(mActivity.getSystemService(Context.WIFI_P2P_SERVICE)).thenReturn(mWifiP2pManager); mResetNetworkConfirm.mActivity = mActivity; } @After public void tearDown() { ShadowRecoverySystem.reset(); - ShadowWifiP2pManager.reset(); } @Test @@ -88,11 +100,10 @@ public class ResetNetworkConfirmTest { * Test for WifiP2pManager factoryReset method. */ @Test - @Ignore public void testResetNetworkData_resetP2p() { mResetNetworkConfirm.p2pFactoryReset(mActivity); - assertThat(ShadowWifiP2pManager.getFactoryResetCount()).isEqualTo(1); + verify(mWifiP2pManager).factoryReset(any(WifiP2pManager.Channel.class), any()); } @Test From cee6346ce9c5dfe4fc47bb603cd3c291b7db36ad Mon Sep 17 00:00:00 2001 From: Stanley Wang Date: Fri, 21 Jan 2022 16:40:57 +0800 Subject: [PATCH 09/11] Ignore tests to avoid Settings presubmit test failure. Bug: 215660413 Test: run robo test Change-Id: I9cb04f5fee9463d40954509c087eb428f16e3b68 --- .../android/settings/dream/DreamPickerControllerTest.java | 2 ++ .../enterprise/BugReportsPreferenceControllerTest.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java b/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java index 1048970cd46..a570cc3e4ce 100644 --- a/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java +++ b/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java @@ -36,6 +36,7 @@ import com.android.settingslib.dream.DreamBackend.DreamInfo; import com.android.settingslib.widget.LayoutPreference; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -47,6 +48,7 @@ import java.util.ArrayList; import java.util.Collections; @RunWith(RobolectricTestRunner.class) +@Ignore public class DreamPickerControllerTest { private DreamPickerController mController; @Mock(answer = Answers.RETURNS_DEEP_STUBS) diff --git a/tests/robotests/src/com/android/settings/enterprise/BugReportsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/enterprise/BugReportsPreferenceControllerTest.java index 5a05eccf640..34853c68e14 100644 --- a/tests/robotests/src/com/android/settings/enterprise/BugReportsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/enterprise/BugReportsPreferenceControllerTest.java @@ -22,13 +22,13 @@ import static org.mockito.Mockito.when; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import java.util.Date; -import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class BugReportsPreferenceControllerTest - extends AdminActionPreferenceControllerTestBase { + extends AdminActionPreferenceControllerTestBase { @Override public void setUp() { @@ -39,7 +39,7 @@ public class BugReportsPreferenceControllerTest @Override public void setDate(Date date) { when(mFeatureFactory.enterprisePrivacyFeatureProvider.getLastBugReportRequestTime()) - .thenReturn(date); + .thenReturn(date); } @Test From 9de30f08e6dd9937e056f03c1c8a84e1e2a38511 Mon Sep 17 00:00:00 2001 From: Jack Yu Date: Tue, 4 Jan 2022 17:40:55 +0800 Subject: [PATCH 10/11] New UX for the NFC default payment settings Re-design the UX. Bug: 202367033 Test: maunal make RunSettingsRoboTests ROBOTEST_FILTER=NfcForegroundPreferenceControllerTest make RunSettingsRoboTests ROBOTEST_FILTER=PaymentSettingsTest Change-Id: I4c05ac4a1974645c76a37900d64aa8e1a491bca1 --- res/values/arrays.xml | 11 + res/values/strings.xml | 22 +- res/xml/nfc_default_payment_settings.xml | 19 ++ res/xml/nfc_payment_settings.xml | 17 +- .../settings/nfc/DefaultPaymentSettings.java | 278 ++++++++++++++++++ ...NfcDefaultPaymentPreferenceController.java | 146 +++++++++ .../NfcForegroundPreferenceController.java | 55 ++-- .../android/settings/nfc/PaymentBackend.java | 3 + .../android/settings/nfc/PaymentSettings.java | 34 ++- ...NfcForegroundPreferenceControllerTest.java | 30 +- .../settings/nfc/PaymentSettingsTest.java | 4 +- 11 files changed, 540 insertions(+), 79 deletions(-) create mode 100644 res/xml/nfc_default_payment_settings.xml create mode 100644 src/com/android/settings/nfc/DefaultPaymentSettings.java create mode 100644 src/com/android/settings/nfc/NfcDefaultPaymentPreferenceController.java diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 9f0af97fa5c..4c515ad441d 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -1596,4 +1596,15 @@ 2 4 + + + + Always + Except when another payment app is open + + + + 0 + 1 + diff --git a/res/values/strings.xml b/res/values/strings.xml index ec72321ced8..fab3500cbf9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7596,7 +7596,25 @@ Install available apps + Contactless payments + + Default payment app + + To make a payment using a payment app, hold the back of your device to a payment terminal + + Learn more + + + + Set work app as default payment app? + + To make a payment using a work app: + + work profile must be turned on. + + you\u2019ll need to enter your work profile lock if you have one. + How it works @@ -7608,7 +7626,9 @@ %1$s - %2$s - Use default + Use default payment app + + Use default payment app Always diff --git a/res/xml/nfc_default_payment_settings.xml b/res/xml/nfc_default_payment_settings.xml new file mode 100644 index 00000000000..d9978cdf5c2 --- /dev/null +++ b/res/xml/nfc_default_payment_settings.xml @@ -0,0 +1,19 @@ + + + + diff --git a/res/xml/nfc_payment_settings.xml b/res/xml/nfc_payment_settings.xml index 8e55a39a3ca..d748a1c20b3 100644 --- a/res/xml/nfc_payment_settings.xml +++ b/res/xml/nfc_payment_settings.xml @@ -19,16 +19,17 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/nfc_payment_settings_title"> - + - + android:dialogTitle="@string/nfc_payment_use_default_dialog" + settings:controller="com.android.settings.nfc.NfcForegroundPreferenceController" + android:entries="@array/nfc_payment_favor" + android:entryValues="@array/nfc_payment_favor_values" /> diff --git a/src/com/android/settings/nfc/DefaultPaymentSettings.java b/src/com/android/settings/nfc/DefaultPaymentSettings.java new file mode 100644 index 00000000000..4dceefc3451 --- /dev/null +++ b/src/com/android/settings/nfc/DefaultPaymentSettings.java @@ -0,0 +1,278 @@ +/* + * 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.nfc; + +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.Layout; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.AlignmentSpan; +import android.text.style.BulletSpan; +import android.text.style.ClickableSpan; +import android.text.style.RelativeSizeSpan; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; +import com.android.settings.nfc.PaymentBackend.PaymentAppInfo; +import com.android.settingslib.widget.CandidateInfo; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * DefaultPaymentSettings handles the NFC default payment app selection. + */ +public class DefaultPaymentSettings extends DefaultAppPickerFragment { + public static final String TAG = "DefaultPaymentSettings"; + + private PaymentBackend mPaymentBackend; + private List mAppInfos; + private Preference mFooterPreference; + + @Override + public int getMetricsCategory() { + return SettingsEnums.NFC_PAYMENT; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.nfc_default_payment_settings; + } + + @Override + protected String getDefaultKey() { + PaymentAppInfo defaultAppInfo = mPaymentBackend.getDefaultApp(); + if (defaultAppInfo != null) { + return defaultAppInfo.componentName.flattenToString() + " " + + defaultAppInfo.userHandle.getIdentifier(); + } + return null; + } + + @Override + protected boolean setDefaultKey(String key) { + String[] keys = key.split(" "); + if (keys.length >= 2) { + mPaymentBackend.setDefaultPaymentApp(ComponentName.unflattenFromString(keys[0]), + Integer.parseInt(keys[1])); + } + return true; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mPaymentBackend = new PaymentBackend(getActivity()); + mAppInfos = mPaymentBackend.getPaymentAppInfos(); + } + + @Override + protected void addStaticPreferences(PreferenceScreen screen) { + if (mFooterPreference == null) { + setupFooterPreference(); + } + screen.addPreference(mFooterPreference); + } + + @Override + public void onResume() { + super.onResume(); + mPaymentBackend.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + mPaymentBackend.onPause(); + } + + /** + * Comparator for NfcPaymentCandidateInfo. + */ + public class NfcPaymentCandidateInfoComparator implements Comparator { + /** + * Compare the NfcPaymentCandidateInfo by the label string. + */ + public int compare(NfcPaymentCandidateInfo obj1, NfcPaymentCandidateInfo obj2) { + if (obj1.loadLabel() == obj2.loadLabel()) { + return 0; + } + if (obj1.loadLabel() == null) { + return -1; + } + if (obj2.loadLabel() == null) { + return 1; + } + return obj1.loadLabel().toString().compareTo(obj2.loadLabel().toString()); + } + } + + @Override + public void bindPreferenceExtra(SelectorWithWidgetPreference pref, String key, + CandidateInfo info, String defaultKey, String systemDefaultKey) { + final NfcPaymentCandidateInfo candidateInfo = (NfcPaymentCandidateInfo) info; + if (candidateInfo.isManagedProfile()) { + pref.setSummary("Work"); + } + } + + @Override + protected List getCandidates() { + final List candidates = new ArrayList<>(); + for (PaymentAppInfo appInfo: mAppInfos) { + UserManager um = getContext().createContextAsUser( + appInfo.userHandle, /*flags=*/0).getSystemService(UserManager.class); + boolean isManagedProfile = um.isManagedProfile(appInfo.userHandle.getIdentifier()); + + CharSequence label; + label = appInfo.label; + candidates.add(new NfcPaymentCandidateInfo( + appInfo.componentName.flattenToString(), + label, + appInfo.icon, + appInfo.userHandle.getIdentifier(), + isManagedProfile)); + } + Collections.sort(candidates, new NfcPaymentCandidateInfoComparator()); + return candidates; + } + + @VisibleForTesting + class NfcPaymentCandidateInfo extends CandidateInfo { + private final String mKey; + private final CharSequence mLabel; + private final Drawable mDrawable; + private final int mUserId; + private final boolean mIsManagedProfile; + + NfcPaymentCandidateInfo(String key, CharSequence label, Drawable drawable, int userId, + boolean isManagedProfile) { + super(true /* enabled */); + mKey = key; + mLabel = label; + mDrawable = drawable; + mUserId = userId; + mIsManagedProfile = isManagedProfile; + } + + @Override + public CharSequence loadLabel() { + return mLabel; + } + + @Override + public Drawable loadIcon() { + return mDrawable; + } + + @Override + public String getKey() { + return mKey + " " + mUserId; + } + + public boolean isManagedProfile() { + return mIsManagedProfile; + } + } + + @Override + protected CharSequence getConfirmationMessage(CandidateInfo appInfo) { + if (appInfo == null) { + return null; + } + NfcPaymentCandidateInfo paymentInfo = (NfcPaymentCandidateInfo) appInfo; + UserManager um = getContext().createContextAsUser(UserHandle.of(paymentInfo.mUserId), + /*flags=*/0).getSystemService(UserManager.class); + boolean isManagedProfile = um.isManagedProfile(paymentInfo.mUserId); + if (!isManagedProfile) { + return null; + } + + final String title = getContext().getString( + R.string.nfc_default_payment_workapp_confirmation_title); + final String messageTitle = getContext().getString( + R.string.nfc_default_payment_workapp_confirmation_message_title); + final String messageOne = getContext().getString( + R.string.nfc_default_payment_workapp_confirmation_message_1); + final String messageTwo = getContext().getString( + R.string.nfc_default_payment_workapp_confirmation_message_2); + final SpannableString titleString = new SpannableString(title); + final SpannableString messageString = new SpannableString(messageTitle); + final SpannableString oneString = new SpannableString(messageOne); + final SpannableString twoString = new SpannableString(messageTwo); + + titleString.setSpan(new RelativeSizeSpan(1.5f), 0, title.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + titleString.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + messageString.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, + messageTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + oneString.setSpan(new BulletSpan(20), 0, messageOne.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + twoString.setSpan(new BulletSpan(20), 0, messageTwo.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + return TextUtils.concat(titleString, "\n\n", messageString, "\n\n", oneString, "\n", + twoString); + } + + private void setupFooterPreference() { + final String textNfcDefaultPaymentFooter = getResources().getString( + R.string.nfc_default_payment_footer); + final String textMoreDetails = getResources().getString(R.string.nfc_more_details); + + final SpannableString spannableString = new SpannableString( + textNfcDefaultPaymentFooter + System.lineSeparator() + + System.lineSeparator() + textMoreDetails); + final ClickableSpan clickableSpan = new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + Intent howItWorksIntent = new Intent(getActivity(), HowItWorks.class); + startActivity(howItWorksIntent); + } + }; + + if (textNfcDefaultPaymentFooter != null && textMoreDetails != null) { + spannableString.setSpan(clickableSpan, textNfcDefaultPaymentFooter.length() + 1, + textNfcDefaultPaymentFooter.length() + textMoreDetails.length() + 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + mFooterPreference = new FooterPreference(getContext()); + mFooterPreference.setLayoutResource(R.layout.preference_footer); + mFooterPreference.setTitle(spannableString); + mFooterPreference.setSelectable(false); + mFooterPreference.setIcon(R.drawable.ic_info_outline_24dp); + } +} diff --git a/src/com/android/settings/nfc/NfcDefaultPaymentPreferenceController.java b/src/com/android/settings/nfc/NfcDefaultPaymentPreferenceController.java new file mode 100644 index 00000000000..6bcacb1ad63 --- /dev/null +++ b/src/com/android/settings/nfc/NfcDefaultPaymentPreferenceController.java @@ -0,0 +1,146 @@ +/* + * 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.nfc; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.nfc.NfcAdapter; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.applications.defaultapps.DefaultAppPreferenceController; +import com.android.settings.nfc.PaymentBackend.PaymentAppInfo; +import com.android.settingslib.applications.DefaultAppInfo; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; + +import java.util.List; + +/** + * NfcDefaultPaymentPreferenceController shows an app icon and text summary for current selected + * default payment, and links to the nfc default payment selection page. + */ +public class NfcDefaultPaymentPreferenceController extends DefaultAppPreferenceController implements + PaymentBackend.Callback, LifecycleObserver, OnResume, OnPause { + + private static final String TAG = "NfcDefaultPaymentController"; + private static final String KEY = "nfc_payment_app"; + + private PaymentBackend mPaymentBackend; + private Preference mPreference; + private Context mContext; + + public NfcDefaultPaymentPreferenceController(Context context, Lifecycle lifecycle) { + super(context); + mContext = context; + mPaymentBackend = new PaymentBackend(context); + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @Override + public boolean isAvailable() { + final PackageManager pm = mContext.getPackageManager(); + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC)) { + return false; + } + if (NfcAdapter.getDefaultAdapter(mContext) == null) { + return false; + } + if (mPaymentBackend == null) { + mPaymentBackend = new PaymentBackend(mContext); + } + final List appInfos = mPaymentBackend.getPaymentAppInfos(); + return (appInfos != null && !appInfos.isEmpty()) + ? true + : false; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void onResume() { + if (mPaymentBackend != null) { + mPaymentBackend.registerCallback(this); + mPaymentBackend.onResume(); + } + } + + @Override + public void onPause() { + if (mPaymentBackend != null) { + mPaymentBackend.unregisterCallback(this); + mPaymentBackend.onPause(); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreference = screen.findPreference(getPreferenceKey()); + super.displayPreference(screen); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setIconSpaceReserved(true); + } + + @Override + public void onPaymentAppsChanged() { + updateState(mPreference); + } + + /** + * PaymentDefaultAppInfo is used to store the default payment app info. + */ + public static class PaymentDefaultAppInfo extends DefaultAppInfo { + public PaymentAppInfo mInfo; + + public PaymentDefaultAppInfo(Context context, PackageManager pm, int userId, + PaymentAppInfo info) { + super(context, pm, userId, info.componentName); + mInfo = info; + } + + @Override + public Drawable loadIcon() { + return mInfo.icon; + } + } + + @Override + protected DefaultAppInfo getDefaultAppInfo() { + if (mPaymentBackend == null) { + return null; + } + final PaymentAppInfo defaultApp = mPaymentBackend.getDefaultApp(); + if (defaultApp != null) { + return new PaymentDefaultAppInfo(mContext, mPackageManager, + defaultApp.userHandle.getIdentifier(), defaultApp); + } + return null; + } +} diff --git a/src/com/android/settings/nfc/NfcForegroundPreferenceController.java b/src/com/android/settings/nfc/NfcForegroundPreferenceController.java index b02608ef879..246bdb9716e 100644 --- a/src/com/android/settings/nfc/NfcForegroundPreferenceController.java +++ b/src/com/android/settings/nfc/NfcForegroundPreferenceController.java @@ -16,11 +16,9 @@ package com.android.settings.nfc; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.PackageManager; -import android.text.TextUtils; -import androidx.preference.DropDownPreference; +import androidx.preference.ListPreference; import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; @@ -36,13 +34,18 @@ public class NfcForegroundPreferenceController extends BasePreferenceController PaymentBackend.Callback, Preference.OnPreferenceChangeListener, LifecycleObserver, OnStart, OnStop { - private DropDownPreference mPreference; + private ListPreference mPreference; private PaymentBackend mPaymentBackend; private MetricsFeatureProvider mMetricsFeatureProvider; + private final String[] mListValues; + private final String[] mListEntries; + public NfcForegroundPreferenceController(Context context, String key) { super(context, key); mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + mListValues = context.getResources().getStringArray(R.array.nfc_payment_favor_values); + mListEntries = context.getResources().getStringArray(R.array.nfc_payment_favor); } public void setPaymentBackend(PaymentBackend backend) { @@ -78,21 +81,6 @@ public class NfcForegroundPreferenceController extends BasePreferenceController : UNSUPPORTED_ON_DEVICE; } - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(getPreferenceKey()); - if (mPreference == null) { - return; - } - - mPreference.setEntries(new CharSequence[]{ - mContext.getText(R.string.nfc_payment_favor_open), - mContext.getText(R.string.nfc_payment_favor_default) - }); - mPreference.setEntryValues(new CharSequence[]{"1", "0"}); - } - @Override public void onPaymentAppsChanged() { updateState(mPreference); @@ -100,26 +88,29 @@ public class NfcForegroundPreferenceController extends BasePreferenceController @Override public void updateState(Preference preference) { - if (preference instanceof DropDownPreference) { - ((DropDownPreference) preference).setValue( - mPaymentBackend.isForegroundMode() ? "1" : "0"); - } super.updateState(preference); + if (!(preference instanceof ListPreference)) { + return; + } + final ListPreference listPreference = (ListPreference) preference; + listPreference.setIconSpaceReserved(true); + listPreference.setValue(mListValues[mPaymentBackend.isForegroundMode() ? 1 : 0]); } @Override public CharSequence getSummary() { - return mPreference.getEntry(); + return mListEntries[mPaymentBackend.isForegroundMode() ? 1 : 0]; } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - if (!(preference instanceof DropDownPreference)) { + if (!(preference instanceof ListPreference)) { return false; } - final DropDownPreference pref = (DropDownPreference) preference; + + final ListPreference listPreference = (ListPreference) preference; final String newValueString = (String) newValue; - pref.setSummary(pref.getEntries()[pref.findIndexOfValue(newValueString)]); + listPreference.setSummary(mListEntries[listPreference.findIndexOfValue(newValueString)]); final boolean foregroundMode = Integer.parseInt(newValueString) != 0; mPaymentBackend.setForegroundMode(foregroundMode); mMetricsFeatureProvider.action(mContext, @@ -127,12 +118,4 @@ public class NfcForegroundPreferenceController extends BasePreferenceController : SettingsEnums.ACTION_NFC_PAYMENT_ALWAYS_SETTING); return true; } - - @Override - public void updateNonIndexableKeys(List keys) { - final String key = getPreferenceKey(); - if (!TextUtils.isEmpty(key)) { - keys.add(key); - } - } -} \ No newline at end of file +} diff --git a/src/com/android/settings/nfc/PaymentBackend.java b/src/com/android/settings/nfc/PaymentBackend.java index 542c95bb6d6..0cfed201e9c 100644 --- a/src/com/android/settings/nfc/PaymentBackend.java +++ b/src/com/android/settings/nfc/PaymentBackend.java @@ -20,6 +20,7 @@ import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; import android.nfc.NfcAdapter; import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.cardemulation.CardEmulation; @@ -50,6 +51,7 @@ public class PaymentBackend { public ComponentName componentName; public ComponentName settingsComponent; public UserHandle userHandle; + public Drawable icon; } /** @@ -131,6 +133,7 @@ public class PaymentBackend { appInfo.settingsComponent = null; } appInfo.description = service.getDescription(); + appInfo.icon = pm.getUserBadgedIcon(service.loadIcon(pm), appInfo.userHandle); appInfos.add(appInfo); } diff --git a/src/com/android/settings/nfc/PaymentSettings.java b/src/com/android/settings/nfc/PaymentSettings.java index bbe5f29d9ae..a1f75bc63d2 100644 --- a/src/com/android/settings/nfc/PaymentSettings.java +++ b/src/com/android/settings/nfc/PaymentSettings.java @@ -18,15 +18,11 @@ package com.android.settings.nfc; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -37,8 +33,12 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.search.SearchIndexable; +import java.util.ArrayList; +import java.util.List; @SearchIndexable public class PaymentSettings extends DashboardFragment { @@ -61,13 +61,24 @@ public class PaymentSettings extends DashboardFragment { return R.xml.nfc_payment_settings; } + @Override + protected List createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, getSettingsLifecycle()); + } + + private static List buildPreferenceControllers(Context context, + Lifecycle lifecycle) { + final List controllers = new ArrayList<>(); + controllers.add(new NfcDefaultPaymentPreferenceController(context, lifecycle)); + + return controllers; + } + @Override public void onAttach(Context context) { super.onAttach(context); mPaymentBackend = new PaymentBackend(getActivity()); - setHasOptionsMenu(true); - use(NfcPaymentPreferenceController.class).setPaymentBackend(mPaymentBackend); use(NfcForegroundPreferenceController.class).setPaymentBackend(mPaymentBackend); } @@ -93,15 +104,6 @@ public class PaymentSettings extends DashboardFragment { mPaymentBackend.onPause(); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - MenuItem menuItem = menu.add(R.string.nfc_payment_how_it_works); - Intent howItWorksIntent = new Intent(getActivity(), HowItWorks.class); - menuItem.setIntent(howItWorksIntent); - menuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER); - } - @VisibleForTesting boolean isShowEmptyImage(PreferenceScreen screen) { for (int i = 0; i < screen.getPreferenceCount(); i++) { @@ -127,4 +129,4 @@ public class PaymentSettings extends DashboardFragment { return pm.hasSystemFeature(PackageManager.FEATURE_NFC); } }; -} \ No newline at end of file +} diff --git a/tests/robotests/src/com/android/settings/nfc/NfcForegroundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/nfc/NfcForegroundPreferenceControllerTest.java index 4c2195c50e9..63fa320fbe2 100644 --- a/tests/robotests/src/com/android/settings/nfc/NfcForegroundPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/nfc/NfcForegroundPreferenceControllerTest.java @@ -26,7 +26,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.PackageManager; -import androidx.preference.DropDownPreference; +import androidx.preference.ListPreference; import androidx.preference.PreferenceScreen; import com.android.settings.R; @@ -39,7 +39,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; @@ -56,7 +55,7 @@ public class NfcForegroundPreferenceControllerTest { private PackageManager mManager; private Context mContext; - private DropDownPreference mPreference; + private ListPreference mPreference; private NfcForegroundPreferenceController mController; private FakeFeatureFactory mFakeFeatureFactory; @@ -67,7 +66,9 @@ public class NfcForegroundPreferenceControllerTest { when(mContext.getPackageManager()).thenReturn(mManager); mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); mController = new NfcForegroundPreferenceController(mContext, PREF_KEY); - mPreference = new DropDownPreference(mContext); + mPreference = new ListPreference(mContext); + mPreference.setEntries(R.array.nfc_payment_favor); + mPreference.setEntryValues(R.array.nfc_payment_favor_values); when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference); } @@ -146,17 +147,14 @@ public class NfcForegroundPreferenceControllerTest { final CharSequence favorDefault = mContext.getText(R.string.nfc_payment_favor_default); final CharSequence favorOpen = mContext.getText(R.string.nfc_payment_favor_open); - assertThat(mPreference.getEntry()).isEqualTo(favorDefault); - assertThat(mPreference.getSummary()).isEqualTo(favorDefault); - - mPreference.setValueIndex(0); - mPreference.callChangeListener(mPreference.getEntryValues()[0]); + mPreference.setValueIndex(1); + mPreference.callChangeListener(mPreference.getEntryValues()[1]); verify(mPaymentBackend).setForegroundMode(true); assertThat(mPreference.getEntry()).isEqualTo(favorOpen); assertThat(mPreference.getSummary()).isEqualTo(favorOpen); - mPreference.setValueIndex(1); - mPreference.callChangeListener(mPreference.getEntryValues()[1]); + mPreference.setValueIndex(0); + mPreference.callChangeListener(mPreference.getEntryValues()[0]); verify(mPaymentBackend).setForegroundMode(false); assertThat(mPreference.getEntry()).isEqualTo(favorDefault); assertThat(mPreference.getSummary()).isEqualTo(favorDefault); @@ -168,16 +166,16 @@ public class NfcForegroundPreferenceControllerTest { mController.displayPreference(mScreen); mController.onPaymentAppsChanged(); - mPreference.setValueIndex(0); - mPreference.callChangeListener(mPreference.getEntryValues()[0]); + mPreference.setValueIndex(1); + mPreference.callChangeListener(mPreference.getEntryValues()[1]); verify(mPaymentBackend).setForegroundMode(true); verify(mFakeFeatureFactory.metricsFeatureProvider).action(mContext, SettingsEnums.ACTION_NFC_PAYMENT_FOREGROUND_SETTING); - mPreference.setValueIndex(1); - mPreference.callChangeListener(mPreference.getEntryValues()[1]); + mPreference.setValueIndex(0); + mPreference.callChangeListener(mPreference.getEntryValues()[0]); verify(mPaymentBackend).setForegroundMode(false); verify(mFakeFeatureFactory.metricsFeatureProvider).action(mContext, SettingsEnums.ACTION_NFC_PAYMENT_ALWAYS_SETTING); } -} \ No newline at end of file +} diff --git a/tests/robotests/src/com/android/settings/nfc/PaymentSettingsTest.java b/tests/robotests/src/com/android/settings/nfc/PaymentSettingsTest.java index 32eedd87b3a..458bc93d38c 100644 --- a/tests/robotests/src/com/android/settings/nfc/PaymentSettingsTest.java +++ b/tests/robotests/src/com/android/settings/nfc/PaymentSettingsTest.java @@ -53,7 +53,7 @@ import java.util.List; @Config(shadows = {PaymentSettingsTest.ShadowPaymentBackend.class, ShadowNfcAdapter.class}) public class PaymentSettingsTest { - static final String PAYMENT_KEY = "nfc_payment"; + static final String PAYMENT_KEY = "nfc_payment_app"; static final String FOREGROUND_KEY = "nfc_foreground"; private Context mContext; @@ -160,4 +160,4 @@ public class PaymentSettingsTest { return mAppInfos; } } -} \ No newline at end of file +} From b92fa1fdaa53d0a934b269313b2fdd8b0c3511bb Mon Sep 17 00:00:00 2001 From: Lucas Silva Date: Fri, 21 Jan 2022 15:42:09 +0000 Subject: [PATCH 11/11] Fix broken DreamPickerControllerTest The dream infos are cached at the time the controller is created, so ensure that we mock out the dream infos before building the controller instance. Test: make -j64 RunSettingsRoboTests ROBOTEST_FILTER="com.android.settings.dream.DreamPickerControllerTest" Bug: 215660413 Change-Id: Ie86f701a46dfc9a0ef185459a1dbf9016c1194fc --- .../dream/DreamPickerControllerTest.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java b/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java index a570cc3e4ce..c6c243665fb 100644 --- a/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java +++ b/tests/robotests/src/com/android/settings/dream/DreamPickerControllerTest.java @@ -36,7 +36,6 @@ import com.android.settingslib.dream.DreamBackend.DreamInfo; import com.android.settingslib.widget.LayoutPreference; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -48,9 +47,7 @@ import java.util.ArrayList; import java.util.Collections; @RunWith(RobolectricTestRunner.class) -@Ignore public class DreamPickerControllerTest { - private DreamPickerController mController; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private DreamBackend mBackend; private Context mContext; @@ -65,31 +62,36 @@ public class DreamPickerControllerTest { mPreference = new LayoutPreference(mContext, R.layout.dream_picker_layout); when(mScreen.findPreference(anyString())).thenReturn(mPreference); + } - mController = new DreamPickerController( + private DreamPickerController buildController() { + final DreamPickerController controller = new DreamPickerController( mContext, /* preferenceKey= */ "test", mBackend); - - mController.displayPreference(mScreen); + controller.displayPreference(mScreen); + return controller; } @Test public void isDisabledIfNoDreamsAvailable() { when(mBackend.getDreamInfos()).thenReturn(new ArrayList<>(0)); - assertThat(mController.isAvailable()).isFalse(); + final DreamPickerController controller = buildController(); + assertThat(controller.isAvailable()).isFalse(); } @Test public void isEnabledIfDreamsAvailable() { when(mBackend.getDreamInfos()).thenReturn(Collections.singletonList(new DreamInfo())); - assertThat(mController.isAvailable()).isTrue(); + final DreamPickerController controller = buildController(); + assertThat(controller.isAvailable()).isTrue(); } @Test public void testDreamDisplayedInList() { when(mBackend.getDreamInfos()).thenReturn(Collections.singletonList(new DreamInfo())); - mController.updateState(mPreference); + final DreamPickerController controller = buildController(); + controller.updateState(mPreference); RecyclerView view = mPreference.findViewById(R.id.dream_list); assertThat(view.getAdapter().getItemCount()).isEqualTo(1); @@ -102,7 +104,8 @@ public class DreamPickerControllerTest { mockDreamInfo.isActive = true; when(mBackend.getDreamInfos()).thenReturn(Collections.singletonList(mockDreamInfo)); - mController.updateState(mPreference); + final DreamPickerController controller = buildController(); + controller.updateState(mPreference); Button view = mPreference.findViewById(R.id.preview_button); view.performClick();