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
17package com.android.internal.app;
18
19import android.app.FragmentManager;
20import android.app.FragmentTransaction;
21import android.app.ListFragment;
22import android.content.Context;
23import android.os.Bundle;
24import android.os.LocaleList;
25import android.text.TextUtils;
26import android.view.Menu;
27import android.view.MenuInflater;
28import android.view.MenuItem;
29import android.view.View;
30import android.widget.ListView;
31import android.widget.SearchView;
32
33import com.android.internal.R;
34
35import java.util.Collections;
36import java.util.HashSet;
37import java.util.Locale;
38import 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 */
47public 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         */
71        void onLocaleSelected(LocaleStore.LocaleInfo locale);
72    }
73
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
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     */
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
134    private void returnToParentFrame() {
135        getFragmentManager().popBackStack(PARENT_FRAGMENT_NAME,
136                FragmentManager.POP_BACK_STACK_INCLUSIVE);
137    }
138
139    @Override
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
162    public boolean onOptionsItemSelected(MenuItem menuItem) {
163        int id = menuItem.getItemId();
164        switch (id) {
165            case android.R.id.home:
166                getFragmentManager().popBackStack();
167                return true;
168        }
169        return super.onOptionsItemSelected(menuItem);
170    }
171
172    @Override
173    public void onResume() {
174        super.onResume();
175
176        if (mParentLocale != null) {
177            getActivity().setTitle(mParentLocale.getFullNameNative());
178        } else {
179            getActivity().setTitle(R.string.language_selection_title);
180        }
181
182        getListView().requestFocus();
183    }
184
185    @Override
186    public void onPause() {
187        super.onPause();
188
189        // Save search status
190        if (mSearchView != null) {
191            mPreviousSearchHadFocus = mSearchView.hasFocus();
192            mPreviousSearch = mSearchView.getQuery();
193        } else {
194            mPreviousSearchHadFocus = false;
195            mPreviousSearch = null;
196        }
197
198        // Save scroll position
199        final ListView list = getListView();
200        final View firstChild = list.getChildAt(0);
201        mFirstVisiblePosition = list.getFirstVisiblePosition();
202        mTopDistance = (firstChild == null) ? 0 : (firstChild.getTop() - list.getPaddingTop());
203    }
204
205    @Override
206    public void onListItemClick(ListView l, View v, int position, long id) {
207        final LocaleStore.LocaleInfo locale =
208                (LocaleStore.LocaleInfo) getListAdapter().getItem(position);
209
210        if (locale.getParent() != null) {
211            if (mListener != null) {
212                mListener.onLocaleSelected(locale);
213            }
214            returnToParentFrame();
215        } else {
216            LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
217                    getContext(), mListener, locale, mTranslatedOnly /* translate only */);
218            if (selector != null) {
219                getFragmentManager().beginTransaction()
220                        .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
221                        .replace(getId(), selector).addToBackStack(null)
222                        .commit();
223            } else {
224                returnToParentFrame();
225            }
226        }
227    }
228
229    @Override
230    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
231        if (mParentLocale == null) {
232            inflater.inflate(R.menu.language_selection_list, menu);
233
234            final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu);
235            mSearchView = (SearchView) searchMenuItem.getActionView();
236
237            mSearchView.setQueryHint(getText(R.string.search_language_hint));
238            mSearchView.setOnQueryTextListener(this);
239
240            // Restore previous search status
241            if (!TextUtils.isEmpty(mPreviousSearch)) {
242                searchMenuItem.expandActionView();
243                mSearchView.setIconified(false);
244                mSearchView.setActivated(true);
245                if (mPreviousSearchHadFocus) {
246                    mSearchView.requestFocus();
247                }
248                mSearchView.setQuery(mPreviousSearch, true /* submit */);
249            } else {
250                mSearchView.setQuery(null, false /* submit */);
251            }
252
253            // Restore previous scroll position
254            getListView().setSelectionFromTop(mFirstVisiblePosition, mTopDistance);
255        }
256    }
257
258    @Override
259    public boolean onQueryTextSubmit(String query) {
260        return false;
261    }
262
263    @Override
264    public boolean onQueryTextChange(String newText) {
265        if (mAdapter != null) {
266            mAdapter.getFilter().filter(newText);
267        }
268        return false;
269    }
270}
271