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.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.res.Configuration;
23import android.text.TextUtils;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.BaseAdapter;
28import android.widget.Filter;
29import android.widget.Filterable;
30import android.widget.TextView;
31
32import com.android.internal.R;
33
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.Locale;
37import java.util.Set;
38
39
40/**
41 * This adapter wraps around a regular ListAdapter for LocaleInfo, and creates 2 sections.
42 *
43 * <p>The first section contains "suggested" languages (usually including a region),
44 * the second section contains all the languages within the original adapter.
45 * The "others" might still include languages that appear in the "suggested" section.</p>
46 *
47 * <p>Example: if we show "German Switzerland" as "suggested" (based on SIM, let's say),
48 * then "German" will still show in the "others" section, clicking on it will only show the
49 * countries for all the other German locales, but not Switzerland
50 * (Austria, Belgium, Germany, Liechtenstein, Luxembourg)</p>
51 */
52public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
53    private static final int TYPE_HEADER_SUGGESTED = 0;
54    private static final int TYPE_HEADER_ALL_OTHERS = 1;
55    private static final int TYPE_LOCALE = 2;
56    private static final int MIN_REGIONS_FOR_SUGGESTIONS = 6;
57
58    private ArrayList<LocaleStore.LocaleInfo> mLocaleOptions;
59    private ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
60    private int mSuggestionCount;
61    private final boolean mCountryMode;
62    private LayoutInflater mInflater;
63
64    private Locale mDisplayLocale = null;
65    // used to potentially cache a modified Context that uses mDisplayLocale
66    private Context mContextOverride = null;
67
68    public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) {
69        mCountryMode = countryMode;
70        mLocaleOptions = new ArrayList<>(localeOptions.size());
71        for (LocaleStore.LocaleInfo li : localeOptions) {
72            if (li.isSuggested()) {
73                mSuggestionCount++;
74            }
75            mLocaleOptions.add(li);
76        }
77    }
78
79    @Override
80    public boolean areAllItemsEnabled() {
81        return false;
82    }
83
84    @Override
85    public boolean isEnabled(int position) {
86        return getItemViewType(position) == TYPE_LOCALE;
87    }
88
89    @Override
90    public int getItemViewType(int position) {
91        if (!showHeaders()) {
92            return TYPE_LOCALE;
93        } else {
94            if (position == 0) {
95                return TYPE_HEADER_SUGGESTED;
96            }
97            if (position == mSuggestionCount + 1) {
98                return TYPE_HEADER_ALL_OTHERS;
99            }
100            return TYPE_LOCALE;
101        }
102    }
103
104    @Override
105    public int getViewTypeCount() {
106        if (showHeaders()) {
107            return 3; // Two headers in addition to the locales
108        } else {
109            return 1; // Locales items only
110        }
111    }
112
113    @Override
114    public int getCount() {
115        if (showHeaders()) {
116            return mLocaleOptions.size() + 2; // 2 extra for the headers
117        } else {
118            return mLocaleOptions.size();
119        }
120    }
121
122    @Override
123    public Object getItem(int position) {
124        int offset = 0;
125        if (showHeaders()) {
126            offset = position > mSuggestionCount ? -2 : -1;
127        }
128
129        return mLocaleOptions.get(position + offset);
130    }
131
132    @Override
133    public long getItemId(int position) {
134        return position;
135    }
136
137    /**
138     * Overrides the locale used to display localized labels. Setting the locale to null will reset
139     * the Adapter to use the default locale for the labels.
140     */
141    public void setDisplayLocale(@NonNull Context context, @Nullable Locale locale) {
142        if (locale == null) {
143            mDisplayLocale = null;
144            mContextOverride = null;
145        } else if (!locale.equals(mDisplayLocale)) {
146            mDisplayLocale = locale;
147            final Configuration configOverride = new Configuration();
148            configOverride.setLocale(locale);
149            mContextOverride = context.createConfigurationContext(configOverride);
150        }
151    }
152
153    private void setTextTo(@NonNull TextView textView, int resId) {
154        if (mContextOverride == null) {
155            textView.setText(resId);
156        } else {
157            textView.setText(mContextOverride.getText(resId));
158            // If mContextOverride is not null, mDisplayLocale can't be null either.
159        }
160    }
161
162    @Override
163    public View getView(int position, View convertView, ViewGroup parent) {
164        if (convertView == null && mInflater == null) {
165            mInflater = LayoutInflater.from(parent.getContext());
166        }
167
168        int itemType = getItemViewType(position);
169        switch (itemType) {
170            case TYPE_HEADER_SUGGESTED: // intentional fallthrough
171            case TYPE_HEADER_ALL_OTHERS:
172                // Covers both null, and "reusing" a wrong kind of view
173                if (!(convertView instanceof TextView)) {
174                    convertView = mInflater.inflate(R.layout.language_picker_section_header,
175                            parent, false);
176                }
177                TextView textView = (TextView) convertView;
178                if (itemType == TYPE_HEADER_SUGGESTED) {
179                    setTextTo(textView, R.string.language_picker_section_suggested);
180                } else {
181                    if (mCountryMode) {
182                        setTextTo(textView, R.string.region_picker_section_all);
183                    } else {
184                        setTextTo(textView, R.string.language_picker_section_all);
185                    }
186                }
187                textView.setTextLocale(
188                        mDisplayLocale != null ? mDisplayLocale : Locale.getDefault());
189                break;
190            default:
191                // Covers both null, and "reusing" a wrong kind of view
192                if (!(convertView instanceof ViewGroup)) {
193                    convertView = mInflater.inflate(R.layout.language_picker_item, parent, false);
194                }
195
196                TextView text = (TextView) convertView.findViewById(R.id.locale);
197                LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
198                text.setText(item.getLabel(mCountryMode));
199                text.setTextLocale(item.getLocale());
200                text.setContentDescription(item.getContentDescription(mCountryMode));
201                if (mCountryMode) {
202                    int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
203                    //noinspection ResourceType
204                    convertView.setLayoutDirection(layoutDir);
205                    text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL
206                            ? View.TEXT_DIRECTION_RTL
207                            : View.TEXT_DIRECTION_LTR);
208                }
209        }
210        return convertView;
211    }
212
213    private boolean showHeaders() {
214        // We don't want to show suggestions for locales with very few regions
215        // (e.g. Romanian, with 2 regions)
216        // So we put a (somewhat) arbitrary limit.
217        //
218        // The initial idea was to make that limit dependent on the screen height.
219        // But that would mean rotating the screen could make the suggestions disappear,
220        // as the number of countries that fits on the screen would be different in portrait
221        // and landscape mode.
222        if (mCountryMode && mLocaleOptions.size() < MIN_REGIONS_FOR_SUGGESTIONS) {
223            return false;
224        }
225        return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size();
226    }
227
228    /**
229     * Sorts the items in the adapter using a locale-aware comparator.
230     * @param comp The locale-aware comparator to use.
231     */
232    public void sort(LocaleHelper.LocaleInfoComparator comp) {
233        Collections.sort(mLocaleOptions, comp);
234    }
235
236    class FilterByNativeAndUiNames extends Filter {
237
238        @Override
239        protected FilterResults performFiltering(CharSequence prefix) {
240            FilterResults results = new FilterResults();
241
242            if (mOriginalLocaleOptions == null) {
243                mOriginalLocaleOptions = new ArrayList<>(mLocaleOptions);
244            }
245
246            ArrayList<LocaleStore.LocaleInfo> values;
247            values = new ArrayList<>(mOriginalLocaleOptions);
248            if (prefix == null || prefix.length() == 0) {
249                results.values = values;
250                results.count = values.size();
251            } else {
252                // TODO: decide if we should use the string's locale
253                Locale locale = Locale.getDefault();
254                String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale);
255
256                final int count = values.size();
257                final ArrayList<LocaleStore.LocaleInfo> newValues = new ArrayList<>();
258
259                for (int i = 0; i < count; i++) {
260                    final LocaleStore.LocaleInfo value = values.get(i);
261                    final String nameToCheck = LocaleHelper.normalizeForSearch(
262                            value.getFullNameInUiLanguage(), locale);
263                    final String nativeNameToCheck = LocaleHelper.normalizeForSearch(
264                            value.getFullNameNative(), locale);
265                    if (wordMatches(nativeNameToCheck, prefixString)
266                            || wordMatches(nameToCheck, prefixString)) {
267                        newValues.add(value);
268                    }
269                }
270
271                results.values = newValues;
272                results.count = newValues.size();
273            }
274
275            return results;
276        }
277
278        // TODO: decide if this is enough, or we want to use a BreakIterator...
279        boolean wordMatches(String valueText, String prefixString) {
280            // First match against the whole, non-split value
281            if (valueText.startsWith(prefixString)) {
282                return true;
283            }
284
285            final String[] words = valueText.split(" ");
286            // Start at index 0, in case valueText starts with space(s)
287            for (String word : words) {
288                if (word.startsWith(prefixString)) {
289                    return true;
290                }
291            }
292
293            return false;
294        }
295
296        @Override
297        protected void publishResults(CharSequence constraint, FilterResults results) {
298            mLocaleOptions = (ArrayList<LocaleStore.LocaleInfo>) results.values;
299
300            mSuggestionCount = 0;
301            for (LocaleStore.LocaleInfo li : mLocaleOptions) {
302                if (li.isSuggested()) {
303                    mSuggestionCount++;
304                }
305            }
306
307            if (results.count > 0) {
308                notifyDataSetChanged();
309            } else {
310                notifyDataSetInvalidated();
311            }
312        }
313    }
314
315    @Override
316    public Filter getFilter() {
317        return new FilterByNativeAndUiNames();
318    }
319}
320