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