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.View;
30 import android.widget.ListView;
31 import android.widget.SearchView;
32 
33 import com.android.internal.R;
34 
35 import java.util.Collections;
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 PARENT_FRAGMENT_NAME = "localeListEditor";
49 
50     private SuggestedLocaleAdapter mAdapter;
51     private LocaleSelectedListener mListener;
52     private Set<LocaleStore.LocaleInfo> mLocaleList;
53     private LocaleStore.LocaleInfo mParentLocale;
54     private boolean mTranslatedOnly = false;
55     private SearchView mSearchView = null;
56     private CharSequence mPreviousSearch = null;
57     private boolean mPreviousSearchHadFocus = false;
58     private int mFirstVisiblePosition = 0;
59     private int mTopDistance = 0;
60 
61     /**
62      * Other classes can register to be notified when a locale was selected.
63      *
64      * <p>This is the mechanism to "return" the result of the selection.</p>
65      */
66     public interface LocaleSelectedListener {
67         /**
68          * The classes that want to retrieve the locale picked should implement this method.
69          * @param locale    the locale picked.
70          */
onLocaleSelected(LocaleStore.LocaleInfo locale)71         void onLocaleSelected(LocaleStore.LocaleInfo locale);
72     }
73 
createCountryPicker(Context context, LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, boolean translatedOnly)74     private static LocalePickerWithRegion createCountryPicker(Context context,
75             LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
76             boolean translatedOnly) {
77         LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
78         boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
79                 translatedOnly);
80         return shouldShowTheList ? localePicker : null;
81     }
82 
createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly)83     public static LocalePickerWithRegion createLanguagePicker(Context context,
84             LocaleSelectedListener listener, boolean translatedOnly) {
85         LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
86         localePicker.setListener(context, listener, /* parent */ null, translatedOnly);
87         return localePicker;
88     }
89 
90     /**
91      * Sets the listener and initializes the locale list.
92      *
93      * <p>Returns true if we need to show the list, false if not.</p>
94      *
95      * <p>Can return false because of an error, trying to show a list of countries,
96      * but no parent locale was provided.</p>
97      *
98      * <p>It can also return false if the caller tries to show the list in country mode and
99      * there is only one country available (i.e. Japanese => Japan).
100      * In this case we don't even show the list, we call the listener with that locale,
101      * "pretending" it was selected, and return false.</p>
102      */
setListener(Context context, LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, boolean translatedOnly)103     private boolean setListener(Context context, LocaleSelectedListener listener,
104             LocaleStore.LocaleInfo parent, boolean translatedOnly) {
105         this.mParentLocale = parent;
106         this.mListener = listener;
107         this.mTranslatedOnly = translatedOnly;
108         setRetainInstance(true);
109 
110         final HashSet<String> langTagsToIgnore = new HashSet<>();
111         if (!translatedOnly) {
112             final LocaleList userLocales = LocalePicker.getLocales();
113             final String[] langTags = userLocales.toLanguageTags().split(",");
114             Collections.addAll(langTagsToIgnore, langTags);
115         }
116 
117         if (parent != null) {
118             mLocaleList = LocaleStore.getLevelLocales(context,
119                     langTagsToIgnore, parent, translatedOnly);
120             if (mLocaleList.size() <= 1) {
121                 if (listener != null && (mLocaleList.size() == 1)) {
122                     listener.onLocaleSelected(mLocaleList.iterator().next());
123                 }
124                 return false;
125             }
126         } else {
127             mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
128                     null /* no parent */, translatedOnly);
129         }
130 
131         return true;
132     }
133 
returnToParentFrame()134     private void returnToParentFrame() {
135         getFragmentManager().popBackStack(PARENT_FRAGMENT_NAME,
136                 FragmentManager.POP_BACK_STACK_INCLUSIVE);
137     }
138 
139     @Override
onCreate(Bundle savedInstanceState)140     public void onCreate(Bundle savedInstanceState) {
141         super.onCreate(savedInstanceState);
142         setHasOptionsMenu(true);
143 
144         if (mLocaleList == null) {
145             // The fragment was killed and restored by the FragmentManager.
146             // At this point we have no data, no listener. Just return, to prevend a NPE.
147             // Fixes b/28748150. Created b/29400003 for a cleaner solution.
148             returnToParentFrame();
149             return;
150         }
151 
152         final boolean countryMode = mParentLocale != null;
153         final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault();
154         mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode);
155         final LocaleHelper.LocaleInfoComparator comp =
156                 new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode);
157         mAdapter.sort(comp);
158         setListAdapter(mAdapter);
159     }
160 
161     @Override
onViewCreated(View view, Bundle savedInstanceState)162     public void onViewCreated(View view, Bundle savedInstanceState) {
163         super.onViewCreated(view, savedInstanceState);
164         // In order to make the list view work with CollapsingToolbarLayout,
165         // we have to enable the nested scrolling feature of the list view.
166         getListView().setNestedScrollingEnabled(true);
167     }
168 
169     @Override
onOptionsItemSelected(MenuItem menuItem)170     public boolean onOptionsItemSelected(MenuItem menuItem) {
171         int id = menuItem.getItemId();
172         switch (id) {
173             case android.R.id.home:
174                 getFragmentManager().popBackStack();
175                 return true;
176         }
177         return super.onOptionsItemSelected(menuItem);
178     }
179 
180     @Override
onResume()181     public void onResume() {
182         super.onResume();
183 
184         if (mParentLocale != null) {
185             getActivity().setTitle(mParentLocale.getFullNameNative());
186         } else {
187             getActivity().setTitle(R.string.language_selection_title);
188         }
189 
190         getListView().requestFocus();
191     }
192 
193     @Override
onPause()194     public void onPause() {
195         super.onPause();
196 
197         // Save search status
198         if (mSearchView != null) {
199             mPreviousSearchHadFocus = mSearchView.hasFocus();
200             mPreviousSearch = mSearchView.getQuery();
201         } else {
202             mPreviousSearchHadFocus = false;
203             mPreviousSearch = null;
204         }
205 
206         // Save scroll position
207         final ListView list = getListView();
208         final View firstChild = list.getChildAt(0);
209         mFirstVisiblePosition = list.getFirstVisiblePosition();
210         mTopDistance = (firstChild == null) ? 0 : (firstChild.getTop() - list.getPaddingTop());
211     }
212 
213     @Override
onListItemClick(ListView l, View v, int position, long id)214     public void onListItemClick(ListView l, View v, int position, long id) {
215         final LocaleStore.LocaleInfo locale =
216                 (LocaleStore.LocaleInfo) getListAdapter().getItem(position);
217 
218         if (locale.getParent() != null) {
219             if (mListener != null) {
220                 mListener.onLocaleSelected(locale);
221             }
222             returnToParentFrame();
223         } else {
224             LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
225                     getContext(), mListener, locale, mTranslatedOnly /* translate only */);
226             if (selector != null) {
227                 getFragmentManager().beginTransaction()
228                         .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
229                         .replace(getId(), selector).addToBackStack(null)
230                         .commit();
231             } else {
232                 returnToParentFrame();
233             }
234         }
235     }
236 
237     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)238     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
239         if (mParentLocale == null) {
240             inflater.inflate(R.menu.language_selection_list, menu);
241 
242             final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu);
243             mSearchView = (SearchView) searchMenuItem.getActionView();
244 
245             mSearchView.setQueryHint(getText(R.string.search_language_hint));
246             mSearchView.setOnQueryTextListener(this);
247 
248             // Restore previous search status
249             if (!TextUtils.isEmpty(mPreviousSearch)) {
250                 searchMenuItem.expandActionView();
251                 mSearchView.setIconified(false);
252                 mSearchView.setActivated(true);
253                 if (mPreviousSearchHadFocus) {
254                     mSearchView.requestFocus();
255                 }
256                 mSearchView.setQuery(mPreviousSearch, true /* submit */);
257             } else {
258                 mSearchView.setQuery(null, false /* submit */);
259             }
260 
261             // Restore previous scroll position
262             getListView().setSelectionFromTop(mFirstVisiblePosition, mTopDistance);
263         }
264     }
265 
266     @Override
onQueryTextSubmit(String query)267     public boolean onQueryTextSubmit(String query) {
268         return false;
269     }
270 
271     @Override
onQueryTextChange(String newText)272     public boolean onQueryTextChange(String newText) {
273         if (mAdapter != null) {
274             mAdapter.getFilter().filter(newText);
275         }
276         return false;
277     }
278 }
279