1 /* 2 * Copyright (C) 2016 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.internal.app; 18 19 import android.app.FragmentManager; 20 import android.app.FragmentTransaction; 21 import android.app.ListFragment; 22 import android.content.Context; 23 import android.os.Bundle; 24 import android.os.LocaleList; 25 import android.text.TextUtils; 26 import android.view.Menu; 27 import android.view.MenuInflater; 28 import android.view.MenuItem; 29 import android.view.MenuItem.OnActionExpandListener; 30 import android.view.View; 31 import android.widget.ListView; 32 import android.widget.SearchView; 33 34 import com.android.internal.R; 35 36 import java.util.HashSet; 37 import java.util.Locale; 38 import java.util.Set; 39 40 /** 41 * A two-step locale picker. It shows a language, then a country. 42 * 43 * <p>It shows suggestions at the top, then the rest of the locales. 44 * Allows the user to search for locales using both their native name and their name in the 45 * default locale.</p> 46 */ 47 public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener { 48 private static final String TAG = LocalePickerWithRegion.class.getSimpleName(); 49 private static final String PARENT_FRAGMENT_NAME = "localeListEditor"; 50 51 private SuggestedLocaleAdapter mAdapter; 52 private LocaleSelectedListener mListener; 53 private LocaleCollectorBase mLocalePickerCollector; 54 private Set<LocaleStore.LocaleInfo> mLocaleList; 55 private LocaleStore.LocaleInfo mParentLocale; 56 private boolean mTranslatedOnly = false; 57 private SearchView mSearchView = null; 58 private CharSequence mPreviousSearch = null; 59 private boolean mPreviousSearchHadFocus = false; 60 private int mFirstVisiblePosition = 0; 61 private int mTopDistance = 0; 62 private CharSequence mTitle = null; 63 private OnActionExpandListener mOnActionExpandListener; 64 private boolean mIsNumberingSystem = false; 65 66 /** 67 * Other classes can register to be notified when a locale was selected. 68 * 69 * <p>This is the mechanism to "return" the result of the selection.</p> 70 */ 71 public interface LocaleSelectedListener { 72 /** 73 * The classes that want to retrieve the locale picked should implement this method. 74 * @param locale the locale picked. 75 */ onLocaleSelected(LocaleStore.LocaleInfo locale)76 void onLocaleSelected(LocaleStore.LocaleInfo locale); 77 } 78 79 /** 80 * The interface which provides the locale list. 81 */ 82 interface LocaleCollectorBase { 83 /** Gets the ignored locale list. */ getIgnoredLocaleList(boolean translatedOnly)84 HashSet<String> getIgnoredLocaleList(boolean translatedOnly); 85 86 /** Gets the supported locale list. */ getSupportedLocaleList(LocaleStore.LocaleInfo parent, boolean translatedOnly, boolean isForCountryMode)87 Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, 88 boolean translatedOnly, boolean isForCountryMode); 89 90 /** Indicates if the class work for specific package. */ hasSpecificPackageName()91 boolean hasSpecificPackageName(); 92 } 93 createNumberingSystemPicker( LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, boolean translatedOnly, OnActionExpandListener onActionExpandListener, LocaleCollectorBase localePickerCollector)94 private static LocalePickerWithRegion createNumberingSystemPicker( 95 LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, 96 boolean translatedOnly, OnActionExpandListener onActionExpandListener, 97 LocaleCollectorBase localePickerCollector) { 98 LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); 99 localePicker.setOnActionExpandListener(onActionExpandListener); 100 localePicker.setIsNumberingSystem(true); 101 boolean shouldShowTheList = localePicker.setListener(listener, parent, 102 translatedOnly, localePickerCollector); 103 return shouldShowTheList ? localePicker : null; 104 } 105 createCountryPicker( LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, boolean translatedOnly, OnActionExpandListener onActionExpandListener, LocaleCollectorBase localePickerCollector)106 private static LocalePickerWithRegion createCountryPicker( 107 LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, 108 boolean translatedOnly, OnActionExpandListener onActionExpandListener, 109 LocaleCollectorBase localePickerCollector) { 110 LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); 111 localePicker.setOnActionExpandListener(onActionExpandListener); 112 boolean shouldShowTheList = localePicker.setListener(listener, parent, 113 translatedOnly, localePickerCollector); 114 return shouldShowTheList ? localePicker : null; 115 } 116 createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly)117 public static LocalePickerWithRegion createLanguagePicker(Context context, 118 LocaleSelectedListener listener, boolean translatedOnly) { 119 return createLanguagePicker(context, listener, translatedOnly, null, null, null); 120 } 121 createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly, LocaleList explicitLocales)122 public static LocalePickerWithRegion createLanguagePicker(Context context, 123 LocaleSelectedListener listener, boolean translatedOnly, LocaleList explicitLocales) { 124 return createLanguagePicker(context, listener, translatedOnly, explicitLocales, null, null); 125 } 126 127 /** Creates language picker UI */ createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly, LocaleList explicitLocales, String appPackageName, OnActionExpandListener onActionExpandListener)128 public static LocalePickerWithRegion createLanguagePicker(Context context, 129 LocaleSelectedListener listener, boolean translatedOnly, LocaleList explicitLocales, 130 String appPackageName, OnActionExpandListener onActionExpandListener) { 131 LocaleCollectorBase localePickerController; 132 if (TextUtils.isEmpty(appPackageName)) { 133 localePickerController = new SystemLocaleCollector(context, explicitLocales); 134 } else { 135 localePickerController = new AppLocaleCollector(context, appPackageName); 136 } 137 LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); 138 localePicker.setOnActionExpandListener(onActionExpandListener); 139 localePicker.setListener(listener, /* parent */ null, translatedOnly, 140 localePickerController); 141 return localePicker; 142 } 143 setIsNumberingSystem(boolean isNumberingSystem)144 private void setIsNumberingSystem(boolean isNumberingSystem) { 145 mIsNumberingSystem = isNumberingSystem; 146 } 147 148 /** 149 * Sets the listener and initializes the locale list. 150 * 151 * <p>Returns true if we need to show the list, false if not.</p> 152 * 153 * <p>Can return false because of an error, trying to show a list of countries, 154 * but no parent locale was provided.</p> 155 * 156 * <p>It can also return false if the caller tries to show the list in country mode and 157 * there is only one country available (i.e. Japanese => Japan). 158 * In this case we don't even show the list, we call the listener with that locale, 159 * "pretending" it was selected, and return false.</p> 160 */ setListener(LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, boolean translatedOnly, LocaleCollectorBase localePickerController)161 private boolean setListener(LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, 162 boolean translatedOnly, LocaleCollectorBase localePickerController) { 163 this.mParentLocale = parent; 164 this.mListener = listener; 165 this.mTranslatedOnly = translatedOnly; 166 this.mLocalePickerCollector = localePickerController; 167 setRetainInstance(true); 168 169 mLocaleList = localePickerController.getSupportedLocaleList( 170 parent, translatedOnly, parent != null); 171 172 if (parent != null && listener != null && mLocaleList.size() == 1) { 173 listener.onLocaleSelected(mLocaleList.iterator().next()); 174 return false; 175 } else { 176 return true; 177 } 178 } 179 returnToParentFrame()180 private void returnToParentFrame() { 181 getFragmentManager().popBackStack(PARENT_FRAGMENT_NAME, 182 FragmentManager.POP_BACK_STACK_INCLUSIVE); 183 } 184 185 @Override onCreate(Bundle savedInstanceState)186 public void onCreate(Bundle savedInstanceState) { 187 super.onCreate(savedInstanceState); 188 setHasOptionsMenu(true); 189 190 if (mLocaleList == null) { 191 // The fragment was killed and restored by the FragmentManager. 192 // At this point we have no data, no listener. Just return, to prevend a NPE. 193 // Fixes b/28748150. Created b/29400003 for a cleaner solution. 194 returnToParentFrame(); 195 return; 196 } 197 198 mTitle = getActivity().getTitle(); 199 final boolean countryMode = mParentLocale != null; 200 final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault(); 201 final boolean hasSpecificPackageName = 202 mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName(); 203 mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName); 204 mAdapter.setNumberingSystemMode(mIsNumberingSystem); 205 final LocaleHelper.LocaleInfoComparator comp = 206 new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode); 207 mAdapter.sort(comp); 208 setListAdapter(mAdapter); 209 } 210 211 @Override onViewCreated(View view, Bundle savedInstanceState)212 public void onViewCreated(View view, Bundle savedInstanceState) { 213 super.onViewCreated(view, savedInstanceState); 214 // In order to make the list view work with CollapsingToolbarLayout, 215 // we have to enable the nested scrolling feature of the list view. 216 getListView().setNestedScrollingEnabled(true); 217 getListView().setDivider(null); 218 } 219 220 @Override onOptionsItemSelected(MenuItem menuItem)221 public boolean onOptionsItemSelected(MenuItem menuItem) { 222 int id = menuItem.getItemId(); 223 switch (id) { 224 case android.R.id.home: 225 getFragmentManager().popBackStack(); 226 return true; 227 } 228 return super.onOptionsItemSelected(menuItem); 229 } 230 231 @Override onResume()232 public void onResume() { 233 super.onResume(); 234 if (mParentLocale != null) { 235 getActivity().setTitle(mParentLocale.getFullNameNative()); 236 } else { 237 getActivity().setTitle(mTitle); 238 } 239 240 getListView().requestFocus(); 241 } 242 243 @Override onPause()244 public void onPause() { 245 super.onPause(); 246 247 // Save search status 248 if (mSearchView != null) { 249 mPreviousSearchHadFocus = mSearchView.hasFocus(); 250 mPreviousSearch = mSearchView.getQuery(); 251 } else { 252 mPreviousSearchHadFocus = false; 253 mPreviousSearch = null; 254 } 255 256 // Save scroll position 257 final ListView list = getListView(); 258 final View firstChild = list.getChildAt(0); 259 mFirstVisiblePosition = list.getFirstVisiblePosition(); 260 mTopDistance = (firstChild == null) ? 0 : (firstChild.getTop() - list.getPaddingTop()); 261 } 262 263 @Override onListItemClick(ListView parent, View v, int position, long id)264 public void onListItemClick(ListView parent, View v, int position, long id) { 265 final LocaleStore.LocaleInfo locale = 266 (LocaleStore.LocaleInfo) parent.getAdapter().getItem(position); 267 // Special case for resetting the app locale to equal the system locale. 268 boolean isSystemLocale = locale.isSystemLocale(); 269 boolean isRegionLocale = locale.getParent() != null; 270 boolean mayHaveDifferentNumberingSystem = locale.hasNumberingSystems(); 271 272 if (isSystemLocale 273 // The suggeseted locale would contain the country code except an edge case for 274 // SUGGESTION_TYPE_CURRENT where the application itself set the preferred locale. 275 // In this case, onLocaleSelected() will still set the app locale. 276 || locale.isSuggested() 277 || (isRegionLocale && !mayHaveDifferentNumberingSystem) 278 || mIsNumberingSystem) { 279 if (mListener != null) { 280 mListener.onLocaleSelected(locale); 281 } 282 returnToParentFrame(); 283 } else { 284 LocalePickerWithRegion selector; 285 if (mayHaveDifferentNumberingSystem) { 286 selector = 287 LocalePickerWithRegion.createNumberingSystemPicker( 288 mListener, locale, mTranslatedOnly /* translate only */, 289 mOnActionExpandListener, this.mLocalePickerCollector); 290 } else { 291 selector = LocalePickerWithRegion.createCountryPicker( 292 mListener, locale, mTranslatedOnly /* translate only */, 293 mOnActionExpandListener, this.mLocalePickerCollector); 294 } 295 296 if (selector != null) { 297 getFragmentManager().beginTransaction() 298 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) 299 .replace(getId(), selector).addToBackStack(null) 300 .commit(); 301 } else { 302 returnToParentFrame(); 303 } 304 } 305 } 306 307 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)308 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 309 if (mParentLocale == null) { 310 inflater.inflate(R.menu.language_selection_list, menu); 311 312 final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); 313 if (mOnActionExpandListener != null) { 314 searchMenuItem.setOnActionExpandListener(mOnActionExpandListener); 315 } 316 317 mSearchView = (SearchView) searchMenuItem.getActionView(); 318 mSearchView.setQueryHint(getText(R.string.search_language_hint)); 319 mSearchView.setOnQueryTextListener(this); 320 321 // Restore previous search status 322 if (!TextUtils.isEmpty(mPreviousSearch)) { 323 searchMenuItem.expandActionView(); 324 mSearchView.setIconified(false); 325 mSearchView.setActivated(true); 326 if (mPreviousSearchHadFocus) { 327 mSearchView.requestFocus(); 328 } 329 mSearchView.setQuery(mPreviousSearch, true /* submit */); 330 } else { 331 mSearchView.setQuery(null, false /* submit */); 332 } 333 334 // Restore previous scroll position 335 getListView().setSelectionFromTop(mFirstVisiblePosition, mTopDistance); 336 } 337 } 338 339 @Override onQueryTextSubmit(String query)340 public boolean onQueryTextSubmit(String query) { 341 return false; 342 } 343 344 @Override onQueryTextChange(String newText)345 public boolean onQueryTextChange(String newText) { 346 if (mAdapter != null) { 347 mAdapter.getFilter().filter(newText); 348 } 349 return false; 350 } 351 352 /** 353 * Sets OnActionExpandListener to LocalePickerWithRegion to dectect action of search bar. 354 */ setOnActionExpandListener(OnActionExpandListener onActionExpandListener)355 public void setOnActionExpandListener(OnActionExpandListener onActionExpandListener) { 356 mOnActionExpandListener = onActionExpandListener; 357 } 358 } 359