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