UserDictionarySettings.java revision dcf8ba9eb233b4dadd1e198b7ec6ba413c6583c1
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 void onActivityCreated(Bundle savedInstanceState) {
100        super.onActivityCreated(savedInstanceState);
101
102        final Intent intent = getActivity().getIntent();
103        final String localeFromIntent =
104                null == intent ? null : intent.getStringExtra("locale");
105
106        final Bundle arguments = getArguments();
107        final String localeFromArguments =
108                null == arguments ? null : arguments.getString("locale");
109
110        final String locale;
111        if (null != localeFromArguments) {
112            locale = localeFromArguments;
113        } else if (null != localeFromIntent) {
114            locale = localeFromIntent;
115        } else {
116            locale = null;
117        }
118
119        mLocale = locale;
120        mCursor = createCursor(locale);
121        TextView emptyView = (TextView) getView().findViewById(android.R.id.empty);
122        emptyView.setText(R.string.user_dict_settings_empty_text);
123
124        final ListView listView = getListView();
125        listView.setAdapter(createAdapter());
126        listView.setFastScrollEnabled(true);
127        listView.setEmptyView(emptyView);
128
129        setHasOptionsMenu(true);
130
131        if (savedInstanceState != null) {
132            mDialogEditingWord = savedInstanceState.getString(INSTANCE_KEY_DIALOG_EDITING_WORD);
133            mAddedWordAlready = savedInstanceState.getBoolean(INSTANCE_KEY_ADDED_WORD, false);
134        }
135    }
136
137    @Override
138    public void onResume() {
139        super.onResume();
140        final Intent intent = getActivity().getIntent();
141        if (!mAddedWordAlready
142                && intent.getAction().equals("com.android.settings.USER_DICTIONARY_INSERT")) {
143            final String word = intent.getStringExtra(EXTRA_WORD);
144            mAutoReturn = true;
145            if (word != null) {
146                showAddOrEditDialog(word);
147            }
148        }
149    }
150
151    @Override
152    public void onSaveInstanceState(Bundle outState) {
153        super.onSaveInstanceState(outState);
154        outState.putString(INSTANCE_KEY_DIALOG_EDITING_WORD, mDialogEditingWord);
155        outState.putBoolean(INSTANCE_KEY_ADDED_WORD, mAddedWordAlready);
156    }
157
158    private Cursor createCursor(final String locale) {
159        // Locale can be any of:
160        // - The string representation of a locale, as returned by Locale#toString()
161        // - The empty string. This means we want a cursor returning words valid for all locales.
162        // - null. This means we want a cursor for the current locale, whatever this is.
163        // Note that this contrasts with the data inside the database, where NULL means "all
164        // locales" and there should never be an empty string. The confusion is called by the
165        // historical use of null for "all locales".
166        // TODO: it should be easy to make this more readable by making the special values
167        // human-readable, like "all_locales" and "current_locales" strings, provided they
168        // can be guaranteed not to match locales that may exist.
169        if ("".equals(locale)) {
170            // Case-insensitive sort
171            return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
172                    QUERY_SELECTION_ALL_LOCALES, null,
173                    "UPPER(" + UserDictionary.Words.WORD + ")");
174        } else {
175            final String queryLocale = null != locale ? locale : Locale.getDefault().toString();
176            return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
177                    QUERY_SELECTION, new String[] { queryLocale },
178                    "UPPER(" + UserDictionary.Words.WORD + ")");
179        }
180    }
181
182    private ListAdapter createAdapter() {
183        return new MyAdapter(getActivity(),
184                R.layout.user_dictionary_item, mCursor,
185                new String[] { UserDictionary.Words.WORD, UserDictionary.Words._ID },
186                new int[] { android.R.id.text1, R.id.delete_button }, this);
187    }
188
189    @Override
190    public void onListItemClick(ListView l, View v, int position, long id) {
191        String word = getWord(position);
192        if (word != null) {
193            showAddOrEditDialog(word);
194        }
195    }
196
197    @Override
198    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
199        MenuItem actionItem =
200                menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title)
201                .setIcon(R.drawable.ic_menu_add);
202        actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM |
203                MenuItem.SHOW_AS_ACTION_WITH_TEXT);
204    }
205
206    @Override
207    public boolean onOptionsItemSelected(MenuItem item) {
208        showAddOrEditDialog(null);
209        return true;
210    }
211
212    private void showAddOrEditDialog(String editingWord) {
213        mDialogEditingWord = editingWord;
214        showDialog(DIALOG_ADD_OR_EDIT);
215    }
216
217    private String getWord(int position) {
218        if (null == mCursor) return null;
219        mCursor.moveToPosition(position);
220        // Handle a possible race-condition
221        if (mCursor.isAfterLast()) return null;
222
223        return mCursor.getString(
224                mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD));
225    }
226
227    @Override
228    public Dialog onCreateDialog(int id) {
229        final Activity activity = getActivity();
230        final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity);
231        final LayoutInflater inflater = LayoutInflater.from(dialogBuilder.getContext());
232        final View content = inflater.inflate(R.layout.dialog_edittext, null);
233        final EditText editText = (EditText) content.findViewById(R.id.edittext);
234        editText.setText(mDialogEditingWord);
235        // No prediction in soft keyboard mode. TODO: Create a better way to disable prediction
236        editText.setInputType(InputType.TYPE_CLASS_TEXT
237                | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
238
239        AlertDialog dialog = dialogBuilder
240                .setTitle(mDialogEditingWord != null
241                        ? R.string.user_dict_settings_edit_dialog_title
242                        : R.string.user_dict_settings_add_dialog_title)
243                .setView(content)
244                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
245                    public void onClick(DialogInterface dialog, int which) {
246                        onAddOrEditFinished(editText.getText().toString());
247                        if (mAutoReturn) activity.onBackPressed();
248                    }})
249                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
250                    public void onClick(DialogInterface dialog, int which) {
251                        if (mAutoReturn) activity.onBackPressed();
252                    }})
253                .create();
254        dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN |
255                WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
256        return dialog;
257    }
258
259    private void showDialog(int dialogId) {
260        if (mDialogFragment != null) {
261            Log.e(TAG, "Old dialog fragment not null!");
262        }
263        mDialogFragment = new SettingsDialogFragment(this, dialogId);
264        mDialogFragment.show(getActivity().getFragmentManager(), Integer.toString(dialogId));
265    }
266
267    private void onAddOrEditFinished(String word) {
268        if (mDialogEditingWord != null) {
269            // The user was editing a word, so do a delete/add
270            deleteWord(mDialogEditingWord);
271        }
272
273        // Disallow duplicates
274        deleteWord(word);
275
276        // TODO: present UI for picking whether to add word to all locales, or current.
277        if (null == mLocale) {
278            // Null means insert with the default system locale.
279            UserDictionary.Words.addWord(getActivity(), word.toString(),
280                    FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_CURRENT);
281        } else if ("".equals(mLocale)) {
282            // Empty string means insert for all languages.
283            UserDictionary.Words.addWord(getActivity(), word.toString(),
284                    FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_ALL);
285        } else {
286            // TODO: fix the framework so that it can accept a locale when we add a word
287            // to the user dictionary instead of querying the system locale.
288            final Locale prevLocale = Locale.getDefault();
289            Locale.setDefault(Utils.createLocaleFromString(mLocale));
290            UserDictionary.Words.addWord(getActivity(), word.toString(),
291                    FREQUENCY_FOR_USER_DICTIONARY_ADDS, UserDictionary.Words.LOCALE_TYPE_CURRENT);
292            Locale.setDefault(prevLocale);
293        }
294        if (null != mCursor && !mCursor.requery()) {
295            throw new IllegalStateException("can't requery on already-closed cursor.");
296        }
297        mAddedWordAlready = true;
298    }
299
300    private void deleteWord(String word) {
301        getActivity().getContentResolver().delete(
302                UserDictionary.Words.CONTENT_URI, DELETE_SELECTION, new String[] { word });
303    }
304
305    private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer,
306            View.OnClickListener {
307
308        private AlphabetIndexer mIndexer;
309        private UserDictionarySettings mSettings;
310
311        private ViewBinder mViewBinder = new ViewBinder() {
312
313            public boolean setViewValue(View v, Cursor c, int columnIndex) {
314                if (v instanceof ImageView && columnIndex == INDEX_ID) {
315                    v.setOnClickListener(MyAdapter.this);
316                    v.setTag(c.getString(INDEX_WORD));
317                    return true;
318                }
319
320                return false;
321            }
322        };
323
324        public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
325                UserDictionarySettings settings) {
326            super(context, layout, c, from, to);
327
328            mSettings = settings;
329            if (null != c) {
330                final String alphabet = context.getString(
331                        com.android.internal.R.string.fast_scroll_alphabet);
332                final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD);
333                mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet);
334            }
335            setViewBinder(mViewBinder);
336        }
337
338        public int getPositionForSection(int section) {
339            return null == mIndexer ? 0 : mIndexer.getPositionForSection(section);
340        }
341
342        public int getSectionForPosition(int position) {
343            return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position);
344        }
345
346        public Object[] getSections() {
347            return null == mIndexer ? null : mIndexer.getSections();
348        }
349
350        public void onClick(View v) {
351            mSettings.deleteWord((String) v.getTag());
352        }
353    }
354}
355