UserDictionaryAddWordContents.java revision a91561aa58db1c43092c1caecc051a11fa5391c7
1/*
2 * Copyright (C) 2013 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.inputmethod.latin.userdictionary;
18
19import android.app.Activity;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.database.Cursor;
23import android.os.Bundle;
24import android.provider.UserDictionary;
25import android.text.TextUtils;
26import android.view.View;
27import android.widget.EditText;
28
29import com.android.inputmethod.compat.UserDictionaryCompatUtils;
30import com.android.inputmethod.latin.R;
31import com.android.inputmethod.latin.utils.LocaleUtils;
32
33import java.util.ArrayList;
34import java.util.Locale;
35import java.util.TreeSet;
36
37// Caveat: This class is basically taken from
38// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java
39// in order to deal with some devices that have issues with the user dictionary handling
40
41/**
42 * A container class to factor common code to UserDictionaryAddWordFragment
43 * and UserDictionaryAddWordActivity.
44 */
45public class UserDictionaryAddWordContents {
46    public static final String EXTRA_MODE = "mode";
47    public static final String EXTRA_WORD = "word";
48    public static final String EXTRA_SHORTCUT = "shortcut";
49    public static final String EXTRA_LOCALE = "locale";
50    public static final String EXTRA_ORIGINAL_WORD = "originalWord";
51    public static final String EXTRA_ORIGINAL_SHORTCUT = "originalShortcut";
52
53    public static final int MODE_EDIT = 0;
54    public static final int MODE_INSERT = 1;
55
56    /* package */ static final int CODE_WORD_ADDED = 0;
57    /* package */ static final int CODE_CANCEL = 1;
58    /* package */ static final int CODE_ALREADY_PRESENT = 2;
59
60    private static final int FREQUENCY_FOR_USER_DICTIONARY_ADDS = 250;
61
62    private final int mMode; // Either MODE_EDIT or MODE_INSERT
63    private final EditText mWordEditText;
64    private final EditText mShortcutEditText;
65    private String mLocale;
66    private final String mOldWord;
67    private final String mOldShortcut;
68    private String mSavedWord;
69    private String mSavedShortcut;
70
71    /* package */ UserDictionaryAddWordContents(final View view, final Bundle args) {
72        mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text);
73        mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut);
74        if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
75            mShortcutEditText.setVisibility(View.GONE);
76            view.findViewById(R.id.user_dictionary_add_shortcut_label).setVisibility(View.GONE);
77        }
78        final String word = args.getString(EXTRA_WORD);
79        if (null != word) {
80            mWordEditText.setText(word);
81            // Use getText in case the edit text modified the text we set. This happens when
82            // it's too long to be edited.
83            mWordEditText.setSelection(mWordEditText.getText().length());
84        }
85        final String shortcut;
86        if (UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
87            shortcut = args.getString(EXTRA_SHORTCUT);
88            if (null != shortcut && null != mShortcutEditText) {
89                mShortcutEditText.setText(shortcut);
90            }
91            mOldShortcut = args.getString(EXTRA_SHORTCUT);
92        } else {
93            shortcut = null;
94            mOldShortcut = null;
95        }
96        mMode = args.getInt(EXTRA_MODE); // default return value for #getInt() is 0 = MODE_EDIT
97        mOldWord = args.getString(EXTRA_WORD);
98        updateLocale(args.getString(EXTRA_LOCALE));
99    }
100
101    /* package */ UserDictionaryAddWordContents(final View view,
102            final UserDictionaryAddWordContents oldInstanceToBeEdited) {
103        mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text);
104        mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut);
105        mMode = MODE_EDIT;
106        mOldWord = oldInstanceToBeEdited.mSavedWord;
107        mOldShortcut = oldInstanceToBeEdited.mSavedShortcut;
108        updateLocale(mLocale);
109    }
110
111    // locale may be null, this means default locale
112    // It may also be the empty string, which means "all locales"
113    /* package */ void updateLocale(final String locale) {
114        mLocale = null == locale ? Locale.getDefault().toString() : locale;
115    }
116
117    /* package */ void saveStateIntoBundle(final Bundle outState) {
118        outState.putString(EXTRA_WORD, mWordEditText.getText().toString());
119        outState.putString(EXTRA_ORIGINAL_WORD, mOldWord);
120        if (null != mShortcutEditText) {
121            outState.putString(EXTRA_SHORTCUT, mShortcutEditText.getText().toString());
122        }
123        if (null != mOldShortcut) {
124            outState.putString(EXTRA_ORIGINAL_SHORTCUT, mOldShortcut);
125        }
126        outState.putString(EXTRA_LOCALE, mLocale);
127    }
128
129    /* package */ void delete(final Context context) {
130        if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) {
131            // Mode edit: remove the old entry.
132            final ContentResolver resolver = context.getContentResolver();
133            UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver);
134        }
135        // If we are in add mode, nothing was added, so we don't need to do anything.
136    }
137
138    /* package */
139    int apply(final Context context, final Bundle outParameters) {
140        if (null != outParameters) saveStateIntoBundle(outParameters);
141        final ContentResolver resolver = context.getContentResolver();
142        if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) {
143            // Mode edit: remove the old entry.
144            UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver);
145        }
146        final String newWord = mWordEditText.getText().toString();
147        final String newShortcut;
148        if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
149            newShortcut = null;
150        } else if (null == mShortcutEditText) {
151            newShortcut = null;
152        } else {
153            final String tmpShortcut = mShortcutEditText.getText().toString();
154            if (TextUtils.isEmpty(tmpShortcut)) {
155                newShortcut = null;
156            } else {
157                newShortcut = tmpShortcut;
158            }
159        }
160        if (TextUtils.isEmpty(newWord)) {
161            // If the word is somehow empty, don't insert it.
162            return CODE_CANCEL;
163        }
164        mSavedWord = newWord;
165        mSavedShortcut = newShortcut;
166        // If there is no shortcut, and the word already exists in the database, then we
167        // should not insert, because either A. the word exists with no shortcut, in which
168        // case the exact same thing we want to insert is already there, or B. the word
169        // exists with at least one shortcut, in which case it has priority on our word.
170        if (hasWord(newWord, context)) return CODE_ALREADY_PRESENT;
171
172        // Disallow duplicates. If the same word with no shortcut is defined, remove it; if
173        // the same word with the same shortcut is defined, remove it; but we don't mind if
174        // there is the same word with a different, non-empty shortcut.
175        UserDictionarySettings.deleteWord(newWord, null, resolver);
176        if (!TextUtils.isEmpty(newShortcut)) {
177            // If newShortcut is empty we just deleted this, no need to do it again
178            UserDictionarySettings.deleteWord(newWord, newShortcut, resolver);
179        }
180
181        // In this class we use the empty string to represent 'all locales' and mLocale cannot
182        // be null. However the addWord method takes null to mean 'all locales'.
183        UserDictionaryCompatUtils.addWord(context, newWord.toString(),
184                FREQUENCY_FOR_USER_DICTIONARY_ADDS, newShortcut, TextUtils.isEmpty(mLocale) ?
185                        null : LocaleUtils.constructLocaleFromString(mLocale));
186
187        return CODE_WORD_ADDED;
188    }
189
190    private static final String[] HAS_WORD_PROJECTION = { UserDictionary.Words.WORD };
191    private static final String HAS_WORD_SELECTION_ONE_LOCALE = UserDictionary.Words.WORD
192            + "=? AND " + UserDictionary.Words.LOCALE + "=?";
193    private static final String HAS_WORD_SELECTION_ALL_LOCALES = UserDictionary.Words.WORD
194            + "=? AND " + UserDictionary.Words.LOCALE + " is null";
195    private boolean hasWord(final String word, final Context context) {
196        final Cursor cursor;
197        // mLocale == "" indicates this is an entry for all languages. Here, mLocale can't
198        // be null at all (it's ensured by the updateLocale method).
199        if ("".equals(mLocale)) {
200            cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI,
201                      HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ALL_LOCALES,
202                      new String[] { word }, null /* sort order */);
203        } else {
204            cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI,
205                      HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ONE_LOCALE,
206                      new String[] { word, mLocale }, null /* sort order */);
207        }
208        try {
209            if (null == cursor) return false;
210            return cursor.getCount() > 0;
211        } finally {
212            if (null != cursor) cursor.close();
213        }
214    }
215
216    public static class LocaleRenderer {
217        private final String mLocaleString;
218        private final String mDescription;
219        // LocaleString may NOT be null.
220        public LocaleRenderer(final Context context, final String localeString) {
221            mLocaleString = localeString;
222            if (null == localeString) {
223                mDescription = context.getString(R.string.user_dict_settings_more_languages);
224            } else if ("".equals(localeString)) {
225                mDescription = context.getString(R.string.user_dict_settings_all_languages);
226            } else {
227                mDescription = LocaleUtils.constructLocaleFromString(localeString).getDisplayName();
228            }
229        }
230        @Override
231        public String toString() {
232            return mDescription;
233        }
234        public String getLocaleString() {
235            return mLocaleString;
236        }
237        // "More languages..." is null ; "All languages" is the empty string.
238        public boolean isMoreLanguages() {
239            return null == mLocaleString;
240        }
241    }
242
243    private static void addLocaleDisplayNameToList(final Context context,
244            final ArrayList<LocaleRenderer> list, final String locale) {
245        if (null != locale) {
246            list.add(new LocaleRenderer(context, locale));
247        }
248    }
249
250    // Helper method to get the list of locales to display for this word
251    public ArrayList<LocaleRenderer> getLocalesList(final Activity activity) {
252        final TreeSet<String> locales = UserDictionaryList.getUserDictionaryLocalesSet(activity);
253        // Remove our locale if it's in, because we're always gonna put it at the top
254        locales.remove(mLocale); // mLocale may not be null
255        final String systemLocale = Locale.getDefault().toString();
256        // The system locale should be inside. We want it at the 2nd spot.
257        locales.remove(systemLocale); // system locale may not be null
258        locales.remove(""); // Remove the empty string if it's there
259        final ArrayList<LocaleRenderer> localesList = new ArrayList<>();
260        // Add the passed locale, then the system locale at the top of the list. Add an
261        // "all languages" entry at the bottom of the list.
262        addLocaleDisplayNameToList(activity, localesList, mLocale);
263        if (!systemLocale.equals(mLocale)) {
264            addLocaleDisplayNameToList(activity, localesList, systemLocale);
265        }
266        for (final String l : locales) {
267            // TODO: sort in unicode order
268            addLocaleDisplayNameToList(activity, localesList, l);
269        }
270        if (!"".equals(mLocale)) {
271            // If mLocale is "", then we already inserted the "all languages" item, so don't do it
272            addLocaleDisplayNameToList(activity, localesList, ""); // meaning: all languages
273        }
274        localesList.add(new LocaleRenderer(activity, null)); // meaning: select another locale
275        return localesList;
276    }
277
278    public String getCurrentUserDictionaryLocale() {
279        return mLocale;
280    }
281}
282