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