From 55b6541023744e5ceba8f24a32017c8b7d455b8f Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Tue, 21 Jan 2025 06:06:27 +0000 Subject: [PATCH] [Settings] Create new fragment for app language Bug: 388199937 Test: manual Flag: EXEMPT refactor Change-Id: I9afc5039456d7824421138209c62fd1b08f3698c --- .../localepicker/AppLocalePickerFragment.java | 419 ++++++++++++++++++ ...egionAndNumberingSystemPickerFragment.java | 46 +- 2 files changed, 454 insertions(+), 11 deletions(-) create mode 100644 src/com/android/settings/localepicker/AppLocalePickerFragment.java diff --git a/src/com/android/settings/localepicker/AppLocalePickerFragment.java b/src/com/android/settings/localepicker/AppLocalePickerFragment.java new file mode 100644 index 00000000000..ba661d3d53b --- /dev/null +++ b/src/com/android/settings/localepicker/AppLocalePickerFragment.java @@ -0,0 +1,419 @@ +/** + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.localepicker; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.LocaleList; +import android.text.TextUtils; +import android.util.FeatureFlagUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.app.AppLocaleCollector; +import com.android.internal.app.LocaleHelper; +import com.android.internal.app.LocaleStore; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.applications.AppLocaleUtil; +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.google.android.material.appbar.AppBarLayout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * A locale picker fragment to show app languages. + * + *

It shows suggestions at the top, then the rest of the locales. + * Allows the user to search for locales using both their native name and their name in the + * default locale.

+ */ +public class AppLocalePickerFragment extends DashboardFragment implements + SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { + public static final String ARG_PACKAGE_NAME = "package"; + public static final String ARG_PACKAGE_UID = "uid"; + + private static final String TAG = "AppLocalePickerFragment"; + private static final String EXTRA_EXPAND_SEARCH_VIEW = "expand_search_view"; + private static final String KEY_PREFERENCE_APP_LOCALE_LIST = "app_locale_list"; + private static final String KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST = + "app_locale_suggested_list"; + private static final String KEY_PREFERENCE_APP_DISCLAIMER = "app_locale_disclaimer"; + private static final String KEY_PREFERENCE_APP_INTRO = "app_intro"; + private static final String KEY_PREFERENCE_APP_DESCRIPTION = "app_locale_description"; + + @Nullable + private SearchView mSearchView = null; + @Nullable + private SearchFilter mSearchFilter = null; + @Nullable + private List mLocaleOptions; + @Nullable + private List mOriginalLocaleInfos; + @Nullable + private LocaleStore.LocaleInfo mLocaleInfo; + @Nullable + private AppLocaleAllListPreferenceController mAppLocaleAllListPreferenceController; + @Nullable + private AppLocaleSuggestedListPreferenceController mSuggestedListPreferenceController; + private AppBarLayout mAppBarLayout; + private RecyclerView mRecyclerView; + private PreferenceScreen mPreferenceScreen; + private boolean mExpandSearch; + private int mUid; + private Activity mActivity; + @SuppressWarnings("NullAway") + private String mPackageName; + @Nullable + private ApplicationInfo mApplicationInfo; + private boolean mIsNumberingMode; + + @Override + public void onCreate(@NonNull Bundle icicle) { + super.onCreate(icicle); + mActivity = getActivity(); + + if (mActivity.isFinishing()) { + return; + } + + if (TextUtils.isEmpty(mPackageName)) { + Log.d(TAG, "There is no package name."); + return; + } + + if (!canDisplayLocaleUi()) { + Log.w(TAG, "Not allow to display Locale Settings UI."); + return; + } + + mPreferenceScreen = getPreferenceScreen(); + setHasOptionsMenu(true); + mApplicationInfo = getApplicationInfo(mPackageName, mUid); + setupDisclaimerPreference(); + setupIntroPreference(); + setupDescriptionPreference(); + mExpandSearch = mActivity.getIntent().getBooleanExtra(EXTRA_EXPAND_SEARCH_VIEW, false); + if (icicle != null) { + mExpandSearch = icicle.getBoolean(EXTRA_EXPAND_SEARCH_VIEW); + } + + AppLocaleCollector appLocaleCollector = new AppLocaleCollector(mActivity, mPackageName); + Set localeList = appLocaleCollector.getSupportedLocaleList(null, + false, false); + mLocaleOptions = new ArrayList<>(localeList.size()); + } + + @Override + public @NonNull View onCreateView(@NonNull LayoutInflater inflater, + @NonNull ViewGroup container, @NonNull Bundle savedInstanceState) { + mAppBarLayout = mActivity.findViewById(R.id.app_bar); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mRecyclerView = view.findViewById(R.id.recycler_view); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (mSearchView != null) { + outState.putBoolean(EXTRA_EXPAND_SEARCH_VIEW, !mSearchView.isIconified()); + } + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.language_selection_list, menu); + final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); + if (searchMenuItem != null) { + searchMenuItem.setOnActionExpandListener(this); + mSearchView = (SearchView) searchMenuItem.getActionView(); + mSearchView.setQueryHint( + mActivity.getResources().getText(R.string.search_language_hint)); + mSearchView.setOnQueryTextListener(this); + mSearchView.setMaxWidth(Integer.MAX_VALUE); + if (mExpandSearch) { + searchMenuItem.expandActionView(); + } + } + } + + private void setupDisclaimerPreference() { + final Preference pref = mPreferenceScreen.findPreference(KEY_PREFERENCE_APP_DISCLAIMER); + boolean shouldShowPref = pref != null && FeatureFlagUtils.isEnabled( + mActivity, FeatureFlagUtils.SETTINGS_APP_LOCALE_OPT_IN_ENABLED); + pref.setVisible(shouldShowPref); + } + + private void setupIntroPreference() { + final Preference pref = mPreferenceScreen.findPreference(KEY_PREFERENCE_APP_INTRO); + if (pref != null && mApplicationInfo != null) { + pref.setIcon(Utils.getBadgedIcon(mActivity, mApplicationInfo)); + pref.setTitle(mApplicationInfo.loadLabel(mActivity.getPackageManager())); + } + } + + private void setupDescriptionPreference() { + final Preference pref = mPreferenceScreen.findPreference( + KEY_PREFERENCE_APP_DESCRIPTION); + int res = getAppDescription(); + if (pref != null && res != -1) { + pref.setVisible(true); + pref.setTitle(mActivity.getString(res)); + } else { + pref.setVisible(false); + } + } + + private int getAppDescription() { + LocaleList packageLocaleList = AppLocaleUtil.getPackageLocales(mActivity, mPackageName); + String[] assetLocaleList = AppLocaleUtil.getAssetLocales(mActivity, mPackageName); + // TODO add appended url string, "Learn more", to these both sentences. + if ((packageLocaleList != null && packageLocaleList.isEmpty()) + || (packageLocaleList == null && assetLocaleList.length == 0)) { + return R.string.desc_no_available_supported_locale; + } + return -1; + } + + private @Nullable ApplicationInfo getApplicationInfo(String packageName, int userId) { + ApplicationInfo applicationInfo; + try { + applicationInfo = mActivity.getPackageManager() + .getApplicationInfoAsUser(packageName, /* flags= */ 0, userId); + return applicationInfo; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Application info not found for: " + packageName); + return null; + } + } + + private boolean canDisplayLocaleUi() { + try { + PackageManager packageManager = getPackageManager(); + return AppLocaleUtil.canDisplayLocaleUi(mActivity, + packageManager.getApplicationInfo(mPackageName, 0), + packageManager.queryIntentActivities(AppLocaleUtil.LAUNCHER_ENTRY_INTENT, + PackageManager.GET_META_DATA)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to find info for package: " + mPackageName); + } + + return false; + } + + private void filterSearch(@Nullable String query) { + if (mAppLocaleAllListPreferenceController == null) { + Log.d(TAG, "filterSearch(), can not get preference."); + return; + } + + if (mSearchFilter == null) { + mSearchFilter = new SearchFilter(); + } + + mOriginalLocaleInfos = mAppLocaleAllListPreferenceController.getSupportedLocaleList(); + // If we haven't load apps list completely, don't filter anything. + if (mOriginalLocaleInfos == null) { + Log.w(TAG, "Locales haven't loaded completely yet, so nothing can be filtered"); + return; + } + mSearchFilter.filter(query); + } + + private class SearchFilter extends Filter { + + @Override + protected FilterResults performFiltering(CharSequence prefix) { + FilterResults results = new FilterResults(); + + if (mOriginalLocaleInfos == null) { + mOriginalLocaleInfos = new ArrayList<>(mLocaleOptions); + } + + if (TextUtils.isEmpty(prefix)) { + results.values = mOriginalLocaleInfos; + results.count = mOriginalLocaleInfos.size(); + } else { + // TODO: decide if we should use the string's locale + Locale locale = Locale.getDefault(); + String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale); + + final int count = mOriginalLocaleInfos.size(); + final ArrayList newValues = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + final LocaleStore.LocaleInfo value = mOriginalLocaleInfos.get(i); + final String nameToCheck = LocaleHelper.normalizeForSearch( + value.getFullNameInUiLanguage(), locale); + final String nativeNameToCheck = LocaleHelper.normalizeForSearch( + value.getFullNameNative(), locale); + if ((wordMatches(nativeNameToCheck, prefixString) + || wordMatches(nameToCheck, prefixString)) && !newValues.contains( + value)) { + newValues.add(value); + } + } + + results.values = newValues; + results.count = newValues.size(); + } + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + if (mAppLocaleAllListPreferenceController == null + || mSuggestedListPreferenceController == null) { + Log.d(TAG, "publishResults(), can not get preference."); + return; + } + + mLocaleOptions = (ArrayList) results.values; + // Need to scroll to first preference when searching. + if (mRecyclerView != null) { + mRecyclerView.post(() -> mRecyclerView.scrollToPosition(0)); + } + + mAppLocaleAllListPreferenceController.onSearchListChanged(mLocaleOptions, null); + mSuggestedListPreferenceController.onSearchListChanged(mLocaleOptions, null); + } + + // TODO: decide if this is enough, or we want to use a BreakIterator... + private boolean wordMatches(String valueText, String prefixString) { + if (valueText == null) { + return false; + } + + // First match against the whole, non-split value + if (valueText.startsWith(prefixString)) { + return true; + } + + return Arrays.stream(valueText.split(" ")) + .anyMatch(word -> word.startsWith(prefixString)); + } + } + + @Override + public boolean onMenuItemActionExpand(@NonNull MenuItem item) { + // To prevent a large space on tool bar. + mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); + // To prevent user can expand the collapsing tool bar view. + ViewCompat.setNestedScrollingEnabled(mRecyclerView, false); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(@NonNull MenuItem item) { + // We keep the collapsed status after user cancel the search function. + mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); + ViewCompat.setNestedScrollingEnabled(mRecyclerView, true); + return true; + } + + @Override + public boolean onQueryTextSubmit(@Nullable String query) { + return false; + } + + @Override + public boolean onQueryTextChange(@Nullable String newText) { + filterSearch(newText); + return false; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.APPS_LOCALE_LIST; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.app_language_picker; + } + + @Override + protected List createPreferenceControllers(Context context) { + return buildPreferenceControllers(context); + } + + private List buildPreferenceControllers( + @NonNull Context context) { + Bundle args = getArguments(); + mPackageName = args.getString(ARG_PACKAGE_NAME); + mUid = args.getInt(ARG_PACKAGE_UID); + mLocaleInfo = (LocaleStore.LocaleInfo) args.getSerializable( + RegionAndNumberingSystemPickerFragment.EXTRA_TARGET_LOCALE); + mIsNumberingMode = args.getBoolean( + RegionAndNumberingSystemPickerFragment.EXTRA_IS_NUMBERING_SYSTEM); + + mSuggestedListPreferenceController = + new AppLocaleSuggestedListPreferenceController(context, + KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST, mPackageName, mIsNumberingMode, + mLocaleInfo); + mAppLocaleAllListPreferenceController = new AppLocaleAllListPreferenceController( + context, KEY_PREFERENCE_APP_LOCALE_LIST, mPackageName, mIsNumberingMode, + mLocaleInfo); + final List controllers = new ArrayList<>(); + controllers.add(mSuggestedListPreferenceController); + controllers.add(mAppLocaleAllListPreferenceController); + + return controllers; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.app_language_picker); +} diff --git a/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java b/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java index 5ab2c6cdc6f..9831c137915 100644 --- a/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java +++ b/src/com/android/settings/localepicker/RegionAndNumberingSystemPickerFragment.java @@ -71,6 +71,9 @@ public class RegionAndNumberingSystemPickerFragment extends DashboardFragment im private static final String KEY_PREFERENCE_SYSTEM_LOCALE_LIST = "system_locale_list"; private static final String KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST = "system_locale_suggested_list"; + private static final String KEY_PREFERENCE_APP_LOCALE_LIST = "app_locale_list"; + private static final String KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST = + "app_locale_suggested_list"; private static final String KEY_TOP_INTRO_PREFERENCE = "top_intro_region"; private static final String EXTRA_EXPAND_SEARCH_VIEW = "expand_search_view"; @@ -82,6 +85,10 @@ public class RegionAndNumberingSystemPickerFragment extends DashboardFragment im private SystemLocaleAllListPreferenceController mSystemLocaleAllListPreferenceController; @SuppressWarnings("NullAway") private SystemLocaleSuggestedListPreferenceController mSuggestedListPreferenceController; + @SuppressWarnings("NullAway") + private AppLocaleAllListPreferenceController mAppLocaleAllListPreferenceController; + @SuppressWarnings("NullAway") + private AppLocaleSuggestedListPreferenceController mAppLocaleSuggestedListPreferenceController; @Nullable private LocaleStore.LocaleInfo mLocaleInfo; @Nullable @@ -95,6 +102,8 @@ public class RegionAndNumberingSystemPickerFragment extends DashboardFragment im private boolean mIsNumberingMode; @Nullable private CharSequence mPrefix; + @SuppressWarnings("NullAway") + private String mPackageName; @Override public void onCreate(@NonNull Bundle icicle) { @@ -298,21 +307,36 @@ public class RegionAndNumberingSystemPickerFragment extends DashboardFragment im @Override protected List createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getSettingsLifecycle()); + return buildPreferenceControllers(context); } private List buildPreferenceControllers( - @NonNull Context context, @Nullable Lifecycle lifecycle) { + @NonNull Context context) { final List controllers = new ArrayList<>(); - mLocaleInfo = (LocaleStore.LocaleInfo) getArguments().getSerializable(EXTRA_TARGET_LOCALE); - mIsNumberingMode = getArguments().getBoolean(EXTRA_IS_NUMBERING_SYSTEM); - mSuggestedListPreferenceController = new SystemLocaleSuggestedListPreferenceController( - context, KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST, mLocaleInfo, - mIsNumberingMode); - mSystemLocaleAllListPreferenceController = new SystemLocaleAllListPreferenceController( - context, KEY_PREFERENCE_SYSTEM_LOCALE_LIST, mLocaleInfo, mIsNumberingMode); - controllers.add(mSuggestedListPreferenceController); - controllers.add(mSystemLocaleAllListPreferenceController); + Bundle args = getArguments(); + mLocaleInfo = (LocaleStore.LocaleInfo) args.getSerializable(EXTRA_TARGET_LOCALE); + mIsNumberingMode = args.getBoolean(EXTRA_IS_NUMBERING_SYSTEM); + mPackageName = args.getString(EXTRA_APP_PACKAGE_NAME); + Log.d(TAG, "buildPreferenceControllers packageName = " + mPackageName); + if (TextUtils.isEmpty(mPackageName)) { + mSuggestedListPreferenceController = new SystemLocaleSuggestedListPreferenceController( + context, KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST, mLocaleInfo, + mIsNumberingMode); + mSystemLocaleAllListPreferenceController = new SystemLocaleAllListPreferenceController( + context, KEY_PREFERENCE_SYSTEM_LOCALE_LIST, mLocaleInfo, mIsNumberingMode); + controllers.add(mSuggestedListPreferenceController); + controllers.add(mSystemLocaleAllListPreferenceController); + } else { + mAppLocaleSuggestedListPreferenceController = + new AppLocaleSuggestedListPreferenceController(context, + KEY_PREFERENCE_APP_LOCALE_SUGGESTED_LIST, mPackageName, + mIsNumberingMode, mLocaleInfo); + mAppLocaleAllListPreferenceController = new AppLocaleAllListPreferenceController( + context, KEY_PREFERENCE_APP_LOCALE_LIST, mPackageName, mIsNumberingMode, + mLocaleInfo); + controllers.add(mAppLocaleSuggestedListPreferenceController); + controllers.add(mAppLocaleAllListPreferenceController); + } return controllers; }