1 /** 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.localepicker; 18 19 import android.app.Activity; 20 import android.app.settings.SettingsEnums; 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.os.LocaleList; 24 import android.provider.Settings; 25 import android.text.TextUtils; 26 import android.util.Log; 27 import android.view.LayoutInflater; 28 import android.view.Menu; 29 import android.view.MenuInflater; 30 import android.view.MenuItem; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.Filter; 34 import android.widget.SearchView; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.core.view.ViewCompat; 39 import androidx.preference.PreferenceCategory; 40 import androidx.recyclerview.widget.RecyclerView; 41 42 import com.android.internal.app.LocaleHelper; 43 import com.android.internal.app.LocaleStore; 44 import com.android.internal.app.SystemLocaleCollector; 45 import com.android.settings.R; 46 import com.android.settings.dashboard.DashboardFragment; 47 import com.android.settings.search.BaseSearchIndexProvider; 48 import com.android.settingslib.core.AbstractPreferenceController; 49 import com.android.settingslib.core.lifecycle.Lifecycle; 50 import com.android.settingslib.widget.TopIntroPreference; 51 52 import com.google.android.material.appbar.AppBarLayout; 53 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.List; 57 import java.util.Locale; 58 import java.util.Set; 59 60 /** 61 * A locale picker fragment to show system languages. 62 * 63 * <p>It shows suggestions at the top, then the rest of the locales. 64 * Allows the user to search for locales using both their native name and their name in the 65 * default locale.</p> 66 */ 67 public class SystemLocalePickerFragment extends DashboardFragment implements 68 SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { 69 70 private static final String TAG = "SystemLocalePickerFragment"; 71 private static final String EXTRA_EXPAND_SEARCH_VIEW = "expand_search_view"; 72 private static final String EXTRA_SEARCH_VIEW_QUERY = "search_view_query"; 73 private static final String KEY_PREFERENCE_SYSTEM_LOCALE_LIST = "system_locale_list"; 74 private static final String KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST = 75 "system_locale_suggested_list"; 76 private static final String KEY_TOP_INTRO_PREFERENCE = "top_intro_region"; 77 78 @Nullable 79 private SearchView mSearchView = null; 80 @Nullable 81 private SearchFilter mSearchFilter = null; 82 @Nullable 83 private List<LocaleStore.LocaleInfo> mLocaleOptions; 84 @Nullable 85 private List<LocaleStore.LocaleInfo> mOriginalLocaleInfos; 86 @Nullable 87 private SystemLocaleAllListPreferenceController mSystemLocaleAllListPreferenceController; 88 @Nullable 89 private SystemLocaleSuggestedListPreferenceController mSuggestedListPreferenceController; 90 private AppBarLayout mAppBarLayout; 91 private RecyclerView mRecyclerView; 92 private Activity mActivity; 93 private boolean mExpandSearch; 94 private CharSequence mPreviousSearch = null; 95 96 @Override onCreate(@onNull Bundle icicle)97 public void onCreate(@NonNull Bundle icicle) { 98 super.onCreate(icicle); 99 mActivity = getActivity(); 100 if (mActivity.isFinishing()) { 101 return; 102 } 103 setHasOptionsMenu(true); 104 105 mExpandSearch = mActivity.getIntent().getBooleanExtra(EXTRA_EXPAND_SEARCH_VIEW, false); 106 if (icicle != null) { 107 mExpandSearch = icicle.getBoolean(EXTRA_EXPAND_SEARCH_VIEW); 108 mPreviousSearch = icicle.getCharSequence(EXTRA_SEARCH_VIEW_QUERY); 109 } 110 111 SystemLocaleCollector systemLocaleCollector = new SystemLocaleCollector(getContext(), null); 112 Set<LocaleStore.LocaleInfo> localeList = systemLocaleCollector.getSupportedLocaleList(null, 113 false, false); 114 mLocaleOptions = new ArrayList<>(localeList.size()); 115 116 TopIntroPreference topIntroPreference = findPreference(KEY_TOP_INTRO_PREFERENCE); 117 if (topIntroPreference != null) { 118 topIntroPreference.setVisible(false); 119 } 120 121 } 122 123 @Override onCreateView(@onNull LayoutInflater inflater, @NonNull ViewGroup container, @NonNull Bundle savedInstanceState)124 public @NonNull View onCreateView(@NonNull LayoutInflater inflater, 125 @NonNull ViewGroup container, @NonNull Bundle savedInstanceState) { 126 mAppBarLayout = mActivity.findViewById(R.id.app_bar); 127 return super.onCreateView(inflater, container, savedInstanceState); 128 } 129 130 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)131 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 132 super.onViewCreated(view, savedInstanceState); 133 mRecyclerView = view.findViewById(R.id.recycler_view); 134 } 135 136 @Override onSaveInstanceState(@onNull Bundle outState)137 public void onSaveInstanceState(@NonNull Bundle outState) { 138 super.onSaveInstanceState(outState); 139 if (mSearchView != null) { 140 outState.putBoolean(EXTRA_EXPAND_SEARCH_VIEW, !mSearchView.isIconified()); 141 outState.putCharSequence(EXTRA_SEARCH_VIEW_QUERY, mSearchView.getQuery()); 142 } 143 } 144 145 @Override onCreateOptionsMenu(@onNull Menu menu, @NonNull MenuInflater inflater)146 public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { 147 super.onCreateOptionsMenu(menu, inflater); 148 inflater.inflate(R.menu.language_selection_list, menu); 149 final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); 150 if (searchMenuItem != null) { 151 searchMenuItem.setOnActionExpandListener(this); 152 mSearchView = (SearchView) searchMenuItem.getActionView(); 153 mSearchView.setQueryHint( 154 getContext().getResources().getText(R.string.search_language_hint)); 155 mSearchView.setOnQueryTextListener(this); 156 mSearchView.setMaxWidth(Integer.MAX_VALUE); 157 if (mExpandSearch) { 158 searchMenuItem.expandActionView(); 159 } 160 // Restore previous search status 161 if (!TextUtils.isEmpty(mPreviousSearch)) { 162 searchMenuItem.expandActionView(); 163 mSearchView.setIconified(false); 164 mSearchView.setActivated(true); 165 mSearchView.setQuery(mPreviousSearch, true /* submit */); 166 } else { 167 mSearchView.setQuery(null, false /* submit */); 168 } 169 } 170 } 171 filterSearch(@ullable String query)172 private void filterSearch(@Nullable String query) { 173 if (mSystemLocaleAllListPreferenceController == null) { 174 Log.d(TAG, "filterSearch(), can not get preference."); 175 return; 176 } 177 178 if (mSearchFilter == null) { 179 mSearchFilter = new SearchFilter(); 180 } 181 182 mOriginalLocaleInfos = mSystemLocaleAllListPreferenceController.getSupportedLocaleList(); 183 // If we haven't load apps list completely, don't filter anything. 184 if (mOriginalLocaleInfos == null) { 185 Log.w(TAG, "Locales haven't loaded completely yet, so nothing can be filtered"); 186 return; 187 } 188 mSearchFilter.filter(query); 189 } 190 191 private class SearchFilter extends Filter { 192 193 @Override performFiltering(CharSequence prefix)194 protected FilterResults performFiltering(CharSequence prefix) { 195 FilterResults results = new FilterResults(); 196 197 if (mOriginalLocaleInfos == null) { 198 mOriginalLocaleInfos = new ArrayList<>(mLocaleOptions); 199 } 200 201 if (TextUtils.isEmpty(prefix)) { 202 results.values = mOriginalLocaleInfos; 203 results.count = mOriginalLocaleInfos.size(); 204 } else { 205 // TODO: decide if we should use the string's locale 206 Locale locale = Locale.getDefault(); 207 String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale); 208 209 final int count = mOriginalLocaleInfos.size(); 210 final ArrayList<LocaleStore.LocaleInfo> newValues = new ArrayList<>(); 211 212 for (int i = 0; i < count; i++) { 213 final LocaleStore.LocaleInfo value = mOriginalLocaleInfos.get(i); 214 final String nameToCheck = LocaleHelper.normalizeForSearch( 215 value.getFullNameInUiLanguage(), locale); 216 final String nativeNameToCheck = LocaleHelper.normalizeForSearch( 217 value.getFullNameNative(), locale); 218 if ((wordMatches(nativeNameToCheck, prefixString) 219 || wordMatches(nameToCheck, prefixString)) && !newValues.contains( 220 value)) { 221 newValues.add(value); 222 } 223 } 224 225 results.values = newValues; 226 results.count = newValues.size(); 227 } 228 229 return results; 230 } 231 232 @Override publishResults(CharSequence constraint, FilterResults results)233 protected void publishResults(CharSequence constraint, FilterResults results) { 234 if (mSystemLocaleAllListPreferenceController == null 235 || mSuggestedListPreferenceController == null) { 236 Log.d(TAG, "publishResults(), can not get preference."); 237 return; 238 } 239 240 mLocaleOptions = (ArrayList<LocaleStore.LocaleInfo>) results.values; 241 // Need to scroll to first preference when searching. 242 if (mRecyclerView != null) { 243 mRecyclerView.post(() -> mRecyclerView.scrollToPosition(0)); 244 } 245 246 mSystemLocaleAllListPreferenceController.onSearchListChanged(mLocaleOptions, null); 247 mSuggestedListPreferenceController.onSearchListChanged(mLocaleOptions, null); 248 } 249 250 // TODO: decide if this is enough, or we want to use a BreakIterator... wordMatches(String valueText, String prefixString)251 private boolean wordMatches(String valueText, String prefixString) { 252 if (valueText == null) { 253 return false; 254 } 255 256 // First match against the whole, non-split value 257 if (valueText.startsWith(prefixString)) { 258 return true; 259 } 260 261 return Arrays.stream(valueText.split(" ")) 262 .anyMatch(word -> word.startsWith(prefixString)); 263 } 264 } 265 266 @Override onMenuItemActionExpand(@onNull MenuItem item)267 public boolean onMenuItemActionExpand(@NonNull MenuItem item) { 268 // To prevent a large space on tool bar. 269 mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); 270 // To prevent user can expand the collapsing tool bar view. 271 ViewCompat.setNestedScrollingEnabled(mRecyclerView, false); 272 return true; 273 } 274 275 @Override onMenuItemActionCollapse(@onNull MenuItem item)276 public boolean onMenuItemActionCollapse(@NonNull MenuItem item) { 277 // We keep the collapsed status after user cancel the search function. 278 mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); 279 ViewCompat.setNestedScrollingEnabled(mRecyclerView, true); 280 return true; 281 } 282 283 @Override onQueryTextSubmit(@ullable String query)284 public boolean onQueryTextSubmit(@Nullable String query) { 285 return false; 286 } 287 288 @Override onQueryTextChange(@ullable String newText)289 public boolean onQueryTextChange(@Nullable String newText) { 290 filterSearch(newText); 291 return false; 292 } 293 294 @Override getLogTag()295 protected String getLogTag() { 296 return TAG; 297 } 298 299 @Override getMetricsCategory()300 public int getMetricsCategory() { 301 return SettingsEnums.USER_LOCALE_LIST; 302 } 303 304 @Override getPreferenceScreenResId()305 protected int getPreferenceScreenResId() { 306 return R.xml.system_language_picker; 307 } 308 309 @Override createPreferenceControllers(Context context)310 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { 311 return buildPreferenceControllers(context); 312 } 313 buildPreferenceControllers( @onNull Context context)314 private List<AbstractPreferenceController> buildPreferenceControllers( 315 @NonNull Context context) { 316 LocaleList explicitLocales = null; 317 if (isDeviceDemoMode()) { 318 Bundle bundle = getIntent().getExtras(); 319 explicitLocales = bundle == null 320 ? null 321 : bundle.getParcelable(Settings.EXTRA_EXPLICIT_LOCALES, LocaleList.class); 322 Log.i(TAG, "Has explicit locales : " + explicitLocales); 323 } 324 mSuggestedListPreferenceController = 325 new SystemLocaleSuggestedListPreferenceController(context, 326 KEY_PREFERENCE_SYSTEM_LOCALE_SUGGESTED_LIST); 327 mSystemLocaleAllListPreferenceController = new SystemLocaleAllListPreferenceController( 328 context, KEY_PREFERENCE_SYSTEM_LOCALE_LIST, explicitLocales); 329 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 330 mSuggestedListPreferenceController.setFragmentManager(getFragmentManager()); 331 mSystemLocaleAllListPreferenceController.setFragmentManager(getFragmentManager()); 332 controllers.add(mSuggestedListPreferenceController); 333 controllers.add(mSystemLocaleAllListPreferenceController); 334 335 return controllers; 336 } 337 isDeviceDemoMode()338 private boolean isDeviceDemoMode() { 339 return Settings.Global.getInt( 340 getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0) == 1; 341 } 342 343 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 344 new BaseSearchIndexProvider(R.xml.system_language_picker); 345 } 346