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