1/**
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16
17package com.android.settings;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.ListFragment;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.Intent;
26import android.database.Cursor;
27import android.os.Bundle;
28import android.provider.UserDictionary;
29import android.text.InputType;
30import android.util.Log;
31import android.view.LayoutInflater;
32import android.view.Menu;
33import android.view.MenuInflater;
34import android.view.MenuItem;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.WindowManager;
38import android.widget.AlphabetIndexer;
39import android.widget.EditText;
40import android.widget.ImageView;
41import android.widget.ListAdapter;
42import android.widget.ListView;
43import android.widget.SectionIndexer;
44import android.widget.SimpleCursorAdapter;
45import android.widget.TextView;
46
47import com.android.settings.SettingsPreferenceFragment.SettingsDialogFragment;
48
49import java.util.Locale;
50
51public class UserDictionarySettings extends ListFragment implements DialogCreatable {
52    private static final String TAG = "UserDictionarySettings";
53
54    private static final String INSTANCE_KEY_DIALOG_EDITING_WORD = "DIALOG_EDITING_WORD";
55    private static final String INSTANCE_KEY_ADDED_WORD = "DIALOG_ADDED_WORD";
56
57    private static final String[] QUERY_PROJECTION = {
58        UserDictionary.Words._ID, UserDictionary.Words.WORD
59    };
60
61    private static final int INDEX_ID = 0;
62    private static final int INDEX_WORD = 1;
63
64    // Either the locale is empty (means the word is applicable to all locales)
65    // or the word equals our current locale
66    private static final String QUERY_SELECTION =
67            UserDictionary.Words.LOCALE + "=?";
68    private static final String QUERY_SELECTION_ALL_LOCALES =
69            UserDictionary.Words.LOCALE + " is null";
70
71    private static final String DELETE_SELECTION = UserDictionary.Words.WORD + "=?";
72
73    private static final String EXTRA_WORD = "word";
74
75    private static final int OPTIONS_MENU_ADD = Menu.FIRST;
76
77    private static final int DIALOG_ADD_OR_EDIT = 0;
78
79    private static final int FREQUENCY_FOR_USER_DICTIONARY_ADDS = 250;
80
81    /** The word being edited in the dialog (null means the user is adding a word). */
82    private String mDialogEditingWord;
83
84    private Cursor mCursor;
85
86    protected String mLocale;
87
88    private boolean mAddedWordAlready;
89    private boolean mAutoReturn;
90
91    private SettingsDialogFragment mDialogFragment;
92
93    @Override
94    public void onCreate(Bundle savedInstanceState) {
95        super.onCreate(savedInstanceState);
96    }
97
98    @Override
99    public View onCreateView(
100            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
101        return inflater.inflate(
102                com.android.internal.R.layout.preference_list_fragment, container, false);
103    }
104
105    @Override
106    public void onActivityCreated(Bundle savedInstanceState) {
107        super.onActivityCreated(savedInstanceState);
108
109        final Intent intent = getActivity().getIntent();
110        final String localeFromIntent =
111                null == intent ? null : intent.getStringExtra("locale");
112
113        final Bundle arguments = getArguments();
114        final String localeFromArguments =
115                null == arguments ? null : arguments.getString("locale");
116
117        final String locale;
118        if (null != localeFromArguments) {
119            locale = localeFromArguments;
120        } else if (null != localeFromIntent) {
121            locale = localeFromIntent;
122        } else {
123            locale = null;
124        }
125
126        mLocale = locale;
127        mCursor = createCursor(locale);
128        TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
129        emptyView.setText(R.string.user_dict_settings_empty_text);
130
131        final ListView listView = getListView();
132        listView.setAdapter(createAdapter());
133        listView.setFastScrollEnabled(true);
134        listView.setEmptyView(emptyView);
135
136        setHasOptionsMenu(true);
137
138        if (savedInstanceState != null) {
139            mDialogEditingWord = savedInstanceState.getString(INSTANCE_KEY_DIALOG_EDITING_WORD);
140            mAddedWordAlready = savedInstanceState.getBoolean(INSTANCE_KEY_ADDED_WORD, false);
141        }
142    }
143
144    @Override
145    public void onResume() {
146        super.onResume();
147        final Intent intent = getActivity().getIntent();
148        if (!mAddedWordAlready
149                && intent.getAction().equals("com.android.settings.USER_DICTIONARY_INSERT")) {
150            final String word = intent.getStringExtra(EXTRA_WORD);
151            mAutoReturn = true;
152            if (word != null) {
153                showAddOrEditDialog(word);
154            }
155        }
156    }
157
158    @Override
159    public void onSaveInstanceState(Bundle outState) {
160        super.onSaveInstanceState(outState);
161        outState.putString(INSTANCE_KEY_DIALOG_EDITING_WORD, mDialogEditingWord);
162        outState.putBoolean(INSTANCE_KEY_ADDED_WORD, mAddedWordAlready);
163    }
164
165    private Cursor createCursor(final String locale) {
166        // Locale can be any of:
167        // - The string representation of a locale, as returned by Locale#toString()
168        // - The empty string. This means we want a cursor returning words valid for all locales.
169        // - null. This means we want a cursor for the current locale, whatever this is.
170        // Note that this contrasts with the data inside the database, where NULL means "all
171        // locales" and there should never be an empty string. The confusion is called by the
172        // historical use of null for "all locales".
173        // TODO: it should be easy to make this more readable by making the special values
174        // human-readable, like "all_locales" and "current_locales" strings, provided they
175        // can be guaranteed not to match locales that may exist.
176        if ("".equals(locale)) {
177            // Case-insensitive sort
178            return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
179                    QUERY_SELECTION_ALL_LOCALES, null,
180                    "UPPER(" + UserDictionary.Words.WORD + ")");
181        } else {
182            final String queryLocale = null != locale ? locale : Locale.getDefault().toString();
183            return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
184                    QUERY_SELECTION, new String[] { queryLocale },
185                    "UPPER(" + UserDictionary.Words.WORD + ")");
186        }
187    }
188
189    private ListAdapter createAdapter() {
190        return new MyAdapter(getActivity(),
191                R.layout.user_dictionary_item, mCursor,
192                new String[] { UserDictionary.Words.WORD, UserDictionary.Words._ID },
193                new int[] { android.R.id.text1, R.id.delete_button }, this);
194    }
195
196    @Override
197    public void onListItemClick(ListView l, View v, int position, long id) {
198        String word = getWord(position);
199        if (word != null) {
200            showAddOrEditDialog(word);
201        }
202    }
203
204    @Override
205    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
206        MenuItem actionItem =
207                menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title)
208                .setIcon(R.drawable.ic_menu_add);
209        actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM |
210                MenuItem.SHOW_AS_ACTION_WITH_TEXT);
211    }
212
213    @Override
214    public boolean onOptionsItemSelected(MenuItem item) {
215        showAddOrEditDialog(null);
216        return true;
217    }
218
219    private void showAddOrEditDialog(String editingWord) {
220        mDialogEditingWord = editingWord;
221        showDialog(DIALOG_ADD_OR_EDIT);
222    }
223
224    private String getWord(int position) {
225        if (null == mCursor) return null;
226        mCursor.moveToPosition(position);
227        // Handle a possible race-condition
228        if (mCursor.isAfterLast()) return null;
229
230        return mCursor.getString(
231                mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD));
232    }
233
234    @Override
235    public Dialog onCreateDialog(int id) {
236        final Activity activity = getActivity();
237        final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity);
238        final LayoutInflater inflater = LayoutInflater.from(dialogBuilder.getContext());
239        final View content = inflater.inflate(R.layout.dialog_edittext, null);
240        final EditText editText = (EditText) content.findViewById(R.id.edittext);
241        editText.setText(mDialogEditingWord);
242        // No prediction in soft keyboard mode. TODO: Create a better way to disable prediction
243        editText.setInputType(InputType.TYPE_CLASS_TEXT
244                | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
245
246        AlertDialog dialog = dialogBuilder
247                .setTitle(mDialogEditingWord != null
248                        ? R.string.user_dict_settings_edit_dialog_title
249                        : R.string.user_dict_settings_add_dialog_title)
250                .setView(content)
251                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
252                    public void onClick(DialogInterface dialog, int which) {
253                        onAddOrEditFinished(editText.getText().toString());
254                        if (mAutoReturn) activity.onBackPressed();
255                    }})
256                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
257                    public void onClick(DialogInterface dialog, int which) {
258                        if (mAutoReturn) activity.onBackPressed();
259                    }})
260                .create();
261        dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN |
262                WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
263        return dialog;
264    }
265
266    private void showDialog(int dialogId) {
267        if (mDialogFragment != null) {
268            Log.e(TAG, "Old dialog fragment not null!");
269        }
270        mDialogFragment = new SettingsDialogFragment(this, dialogId);
271        mDialogFragment.show(getActivity().getFragmentManager(), Integer.toString(dialogId));
272    }
273
274    private void onAddOrEditFinished(String word) {
275        if (mDialogEditingWord != null) {
276            // The user was editing a word, so do a delete/add
277            deleteWord(mDialogEditingWord);
278        }
279
280        // Disallow duplicates
281        deleteWord(word);
282
283        // TODO: present UI for picking whether to add word to all locales, or current.
284        if (null == mLocale) {
285            // Null means insert with the default system locale.
286            UserDictionary.Words.addWord(getActivity(), word.toString(),
287                    FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_CURRENT);
288        } else if ("".equals(mLocale)) {
289            // Empty string means insert for all languages.
290            UserDictionary.Words.addWord(getActivity(), word.toString(),
291                    FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_ALL);
292        } else {
293            // TODO: fix the framework so that it can accept a locale when we add a word
294            // to the user dictionary instead of querying the system locale.
295            final Locale prevLocale = Locale.getDefault();
296            Locale.setDefault(Utils.createLocaleFromString(mLocale));
297            UserDictionary.Words.addWord(getActivity(), word.toString(),
298                    FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_CURRENT);
299            Locale.setDefault(prevLocale);
300        }
301        if (null != mCursor && !mCursor.requery()) {
302            throw new IllegalStateException("can't requery on already-closed cursor.");
303        }
304        mAddedWordAlready = true;
305    }
306
307    private void deleteWord(String word) {
308        getActivity().getContentResolver().delete(
309                UserDictionary.Words.CONTENT_URI, DELETE_SELECTION, new String[] { word });
310    }
311
312    private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer,
313            View.OnClickListener {
314
315        private AlphabetIndexer mIndexer;
316        private UserDictionarySettings mSettings;
317
318        private ViewBinder mViewBinder = new ViewBinder() {
319
320            public boolean setViewValue(View v, Cursor c, int columnIndex) {
321                if (v instanceof ImageView && columnIndex == INDEX_ID) {
322                    v.setOnClickListener(MyAdapter.this);
323                    v.setTag(c.getString(INDEX_WORD));
324                    return true;
325                }
326
327                return false;
328            }
329        };
330
331        public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
332                UserDictionarySettings settings) {
333            super(context, layout, c, from, to);
334
335            mSettings = settings;
336            if (null != c) {
337                final String alphabet = context.getString(
338                        com.android.internal.R.string.fast_scroll_alphabet);
339                final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD);
340                mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet);
341            }
342            setViewBinder(mViewBinder);
343        }
344
345        public int getPositionForSection(int section) {
346            return null == mIndexer ? 0 : mIndexer.getPositionForSection(section);
347        }
348
349        public int getSectionForPosition(int position) {
350            return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position);
351        }
352
353        public Object[] getSections() {
354            return null == mIndexer ? null : mIndexer.getSections();
355        }
356
357        public void onClick(View v) {
358            mSettings.deleteWord((String) v.getTag());
359        }
360    }
361}
362