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