From 33f2096fead29e5030d1da62523c1195085ad5a4 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Tue, 4 Jan 2022 18:08:28 +0800 Subject: [PATCH] [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); + } }