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 com.android.inputmethod.latin.R; 20 21 import android.app.ListFragment; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.database.Cursor; 26 import android.os.Build; 27 import android.os.Bundle; 28 import android.provider.UserDictionary; 29 import android.text.TextUtils; 30 import android.view.LayoutInflater; 31 import android.view.Menu; 32 import android.view.MenuInflater; 33 import android.view.MenuItem; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.AlphabetIndexer; 37 import android.widget.ListAdapter; 38 import android.widget.ListView; 39 import android.widget.SectionIndexer; 40 import android.widget.SimpleCursorAdapter; 41 import android.widget.TextView; 42 43 import java.util.Locale; 44 45 // Caveat: This class is basically taken from 46 // packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionarySettings.java 47 // in order to deal with some devices that have issues with the user dictionary handling 48 49 public class UserDictionarySettings extends ListFragment { 50 51 public static final boolean IS_SHORTCUT_API_SUPPORTED = 52 Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 53 54 private static final String[] QUERY_PROJECTION_SHORTCUT_UNSUPPORTED = 55 { UserDictionary.Words._ID, UserDictionary.Words.WORD}; 56 private static final String[] QUERY_PROJECTION_SHORTCUT_SUPPORTED = 57 { UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT}; 58 private static final String[] QUERY_PROJECTION = 59 IS_SHORTCUT_API_SUPPORTED ? 60 QUERY_PROJECTION_SHORTCUT_SUPPORTED : QUERY_PROJECTION_SHORTCUT_UNSUPPORTED; 61 62 // The index of the shortcut in the above array. 63 private static final int INDEX_SHORTCUT = 2; 64 65 private static final String[] ADAPTER_FROM_SHORTCUT_UNSUPPORTED = { 66 UserDictionary.Words.WORD, 67 }; 68 69 private static final String[] ADAPTER_FROM_SHORTCUT_SUPPORTED = { 70 UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT 71 }; 72 73 private static final String[] ADAPTER_FROM = IS_SHORTCUT_API_SUPPORTED ? 74 ADAPTER_FROM_SHORTCUT_SUPPORTED : ADAPTER_FROM_SHORTCUT_UNSUPPORTED; 75 76 private static final int[] ADAPTER_TO_SHORTCUT_UNSUPPORTED = { 77 android.R.id.text1, 78 }; 79 80 private static final int[] ADAPTER_TO_SHORTCUT_SUPPORTED = { 81 android.R.id.text1, android.R.id.text2 82 }; 83 84 private static final int[] ADAPTER_TO = IS_SHORTCUT_API_SUPPORTED ? 85 ADAPTER_TO_SHORTCUT_SUPPORTED : ADAPTER_TO_SHORTCUT_UNSUPPORTED; 86 87 // Either the locale is empty (means the word is applicable to all locales) 88 // or the word equals our current locale 89 private static final String QUERY_SELECTION = 90 UserDictionary.Words.LOCALE + "=?"; 91 private static final String QUERY_SELECTION_ALL_LOCALES = 92 UserDictionary.Words.LOCALE + " is null"; 93 94 private static final String DELETE_SELECTION_WITH_SHORTCUT = UserDictionary.Words.WORD 95 + "=? AND " + UserDictionary.Words.SHORTCUT + "=?"; 96 private static final String DELETE_SELECTION_WITHOUT_SHORTCUT = UserDictionary.Words.WORD 97 + "=? AND " + UserDictionary.Words.SHORTCUT + " is null OR " 98 + UserDictionary.Words.SHORTCUT + "=''"; 99 private static final String DELETE_SELECTION_SHORTCUT_UNSUPPORTED = 100 UserDictionary.Words.WORD + "=?"; 101 102 private static final int OPTIONS_MENU_ADD = Menu.FIRST; 103 104 private Cursor mCursor; 105 106 protected String mLocale; 107 108 @Override 109 public void onCreate(Bundle savedInstanceState) { 110 super.onCreate(savedInstanceState); 111 getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary); 112 } 113 114 @Override 115 public View onCreateView( 116 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 117 return inflater.inflate( 118 R.layout.user_dictionary_preference_list_fragment, container, false); 119 } 120 121 @Override 122 public void onActivityCreated(Bundle savedInstanceState) { 123 super.onActivityCreated(savedInstanceState); 124 125 final Intent intent = getActivity().getIntent(); 126 final String localeFromIntent = 127 null == intent ? null : intent.getStringExtra("locale"); 128 129 final Bundle arguments = getArguments(); 130 final String localeFromArguments = 131 null == arguments ? null : arguments.getString("locale"); 132 133 final String locale; 134 if (null != localeFromArguments) { 135 locale = localeFromArguments; 136 } else if (null != localeFromIntent) { 137 locale = localeFromIntent; 138 } else { 139 locale = null; 140 } 141 142 mLocale = locale; 143 // WARNING: The following cursor is never closed! TODO: don't put that in a member, and 144 // make sure all cursors are correctly closed. Also, this comes from a call to 145 // Activity#managedQuery, which has been deprecated for a long time (and which FORBIDS 146 // closing the cursor, so take care when resolving this TODO). We should either use a 147 // regular query and close the cursor, or switch to a LoaderManager and a CursorLoader. 148 mCursor = createCursor(locale); 149 TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); 150 emptyView.setText(R.string.user_dict_settings_empty_text); 151 152 final ListView listView = getListView(); 153 listView.setAdapter(createAdapter()); 154 listView.setFastScrollEnabled(true); 155 listView.setEmptyView(emptyView); 156 157 setHasOptionsMenu(true); 158 // Show the language as a subtitle of the action bar 159 getActivity().getActionBar().setSubtitle( 160 UserDictionarySettingsUtils.getLocaleDisplayName(getActivity(), mLocale)); 161 } 162 163 @Override 164 public void onResume() { 165 super.onResume(); 166 ListAdapter adapter = getListView().getAdapter(); 167 if (adapter != null && adapter instanceof MyAdapter) { 168 // The list view is forced refreshed here. This allows the changes done 169 // in UserDictionaryAddWordFragment (update/delete/insert) to be seen when 170 // user goes back to this view. 171 MyAdapter listAdapter = (MyAdapter) adapter; 172 listAdapter.notifyDataSetChanged(); 173 } 174 } 175 176 @SuppressWarnings("deprecation") 177 private Cursor createCursor(final String locale) { 178 // Locale can be any of: 179 // - The string representation of a locale, as returned by Locale#toString() 180 // - The empty string. This means we want a cursor returning words valid for all locales. 181 // - null. This means we want a cursor for the current locale, whatever this is. 182 // Note that this contrasts with the data inside the database, where NULL means "all 183 // locales" and there should never be an empty string. The confusion is called by the 184 // historical use of null for "all locales". 185 // TODO: it should be easy to make this more readable by making the special values 186 // human-readable, like "all_locales" and "current_locales" strings, provided they 187 // can be guaranteed not to match locales that may exist. 188 if ("".equals(locale)) { 189 // Case-insensitive sort 190 return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, 191 QUERY_SELECTION_ALL_LOCALES, null, 192 "UPPER(" + UserDictionary.Words.WORD + ")"); 193 } 194 final String queryLocale = null != locale ? locale : Locale.getDefault().toString(); 195 return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION, 196 QUERY_SELECTION, new String[] { queryLocale }, 197 "UPPER(" + UserDictionary.Words.WORD + ")"); 198 } 199 200 private ListAdapter createAdapter() { 201 return new MyAdapter(getActivity(), R.layout.user_dictionary_item, mCursor, 202 ADAPTER_FROM, ADAPTER_TO); 203 } 204 205 @Override 206 public void onListItemClick(ListView l, View v, int position, long id) { 207 final String word = getWord(position); 208 final String shortcut = getShortcut(position); 209 if (word != null) { 210 showAddOrEditDialog(word, shortcut); 211 } 212 } 213 214 @Override 215 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 216 if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { 217 final Locale systemLocale = getResources().getConfiguration().locale; 218 if (!TextUtils.isEmpty(mLocale) && !mLocale.equals(systemLocale.toString())) { 219 // Hide the add button for ICS because it doesn't support specifying a locale 220 // for an entry. This new "locale"-aware API has been added in conjunction 221 // with the shortcut API. 222 return; 223 } 224 } 225 MenuItem actionItem = 226 menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) 227 .setIcon(R.drawable.ic_menu_add); 228 actionItem.setShowAsAction( 229 MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); 230 } 231 232 @Override 233 public boolean onOptionsItemSelected(MenuItem item) { 234 if (item.getItemId() == OPTIONS_MENU_ADD) { 235 showAddOrEditDialog(null, null); 236 return true; 237 } 238 return false; 239 } 240 241 /** 242 * Add or edit a word. If editingWord is null, it's an add; otherwise, it's an edit. 243 * @param editingWord the word to edit, or null if it's an add. 244 * @param editingShortcut the shortcut for this entry, or null if none. 245 */ 246 private void showAddOrEditDialog(final String editingWord, final String editingShortcut) { 247 final Bundle args = new Bundle(); 248 args.putInt(UserDictionaryAddWordContents.EXTRA_MODE, null == editingWord 249 ? UserDictionaryAddWordContents.MODE_INSERT 250 : UserDictionaryAddWordContents.MODE_EDIT); 251 args.putString(UserDictionaryAddWordContents.EXTRA_WORD, editingWord); 252 args.putString(UserDictionaryAddWordContents.EXTRA_SHORTCUT, editingShortcut); 253 args.putString(UserDictionaryAddWordContents.EXTRA_LOCALE, mLocale); 254 android.preference.PreferenceActivity pa = 255 (android.preference.PreferenceActivity)getActivity(); 256 pa.startPreferencePanel(UserDictionaryAddWordFragment.class.getName(), 257 args, R.string.user_dict_settings_add_dialog_title, null, null, 0); 258 } 259 260 private String getWord(final int position) { 261 if (null == mCursor) return null; 262 mCursor.moveToPosition(position); 263 // Handle a possible race-condition 264 if (mCursor.isAfterLast()) return null; 265 266 return mCursor.getString( 267 mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD)); 268 } 269 270 private String getShortcut(final int position) { 271 if (!IS_SHORTCUT_API_SUPPORTED) return null; 272 if (null == mCursor) return null; 273 mCursor.moveToPosition(position); 274 // Handle a possible race-condition 275 if (mCursor.isAfterLast()) return null; 276 277 return mCursor.getString( 278 mCursor.getColumnIndexOrThrow(UserDictionary.Words.SHORTCUT)); 279 } 280 281 public static void deleteWord(final String word, final String shortcut, 282 final ContentResolver resolver) { 283 if (!IS_SHORTCUT_API_SUPPORTED) { 284 resolver.delete(UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_SHORTCUT_UNSUPPORTED, 285 new String[] { word }); 286 } else if (TextUtils.isEmpty(shortcut)) { 287 resolver.delete( 288 UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITHOUT_SHORTCUT, 289 new String[] { word }); 290 } else { 291 resolver.delete( 292 UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITH_SHORTCUT, 293 new String[] { word, shortcut }); 294 } 295 } 296 297 private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer { 298 private AlphabetIndexer mIndexer; 299 300 private ViewBinder mViewBinder = new ViewBinder() { 301 302 @Override 303 public boolean setViewValue(final View v, final Cursor c, final int columnIndex) { 304 if (!IS_SHORTCUT_API_SUPPORTED) { 305 // just let SimpleCursorAdapter set the view values 306 return false; 307 } 308 if (columnIndex == INDEX_SHORTCUT) { 309 final String shortcut = c.getString(INDEX_SHORTCUT); 310 if (TextUtils.isEmpty(shortcut)) { 311 v.setVisibility(View.GONE); 312 } else { 313 ((TextView)v).setText(shortcut); 314 v.setVisibility(View.VISIBLE); 315 } 316 v.invalidate(); 317 return true; 318 } 319 320 return false; 321 } 322 }; 323 324 public MyAdapter(final Context context, final int layout, final Cursor c, 325 final String[] from, final int[] to) { 326 super(context, layout, c, from, to, 0 /* flags */); 327 328 if (null != c) { 329 final String alphabet = context.getString(R.string.user_dict_fast_scroll_alphabet); 330 final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD); 331 mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet); 332 } 333 setViewBinder(mViewBinder); 334 } 335 336 @Override 337 public int getPositionForSection(final int section) { 338 return null == mIndexer ? 0 : mIndexer.getPositionForSection(section); 339 } 340 341 @Override 342 public int getSectionForPosition(final int position) { 343 return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position); 344 } 345 346 @Override 347 public Object[] getSections() { 348 return null == mIndexer ? null : mIndexer.getSections(); 349 } 350 } 351 } 352 353