1 /** 2 * Copyright (C) 2011 The Android Open Source Project 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 17 package com.android.inputmethod.dictionarypack; 18 19 import android.app.Activity; 20 import android.content.BroadcastReceiver; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.database.Cursor; 26 import android.net.ConnectivityManager; 27 import android.net.NetworkInfo; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.preference.Preference; 31 import android.preference.PreferenceFragment; 32 import android.preference.PreferenceGroup; 33 import android.text.format.DateUtils; 34 import android.util.Log; 35 import android.view.animation.AnimationUtils; 36 import android.view.LayoutInflater; 37 import android.view.Menu; 38 import android.view.MenuInflater; 39 import android.view.MenuItem; 40 import android.view.View; 41 import android.view.ViewGroup; 42 43 import com.android.inputmethod.latin.R; 44 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.Locale; 48 import java.util.TreeMap; 49 50 /** 51 * Preference screen. 52 */ 53 public final class DictionarySettingsFragment extends PreferenceFragment 54 implements UpdateHandler.UpdateEventListener { 55 private static final String TAG = DictionarySettingsFragment.class.getSimpleName(); 56 57 static final private String DICT_LIST_ID = "list"; 58 static final public String DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT = "clientId"; 59 60 static final private int MENU_UPDATE_NOW = Menu.FIRST; 61 62 private View mLoadingView; 63 private String mClientId; 64 private ConnectivityManager mConnectivityManager; 65 private MenuItem mUpdateNowMenu; 66 private boolean mChangedSettings; 67 private DictionaryListInterfaceState mDictionaryListInterfaceState = 68 new DictionaryListInterfaceState(); 69 private TreeMap<String, WordListPreference> mCurrentPreferenceMap = 70 new TreeMap<String, WordListPreference>(); // never null 71 72 private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() { 73 @Override 74 public void onReceive(final Context context, final Intent intent) { 75 refreshNetworkState(); 76 } 77 }; 78 79 /** 80 * Empty constructor for fragment generation. 81 */ DictionarySettingsFragment()82 public DictionarySettingsFragment() { 83 } 84 85 @Override onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState)86 public View onCreateView(final LayoutInflater inflater, final ViewGroup container, 87 final Bundle savedInstanceState) { 88 final View v = inflater.inflate(R.layout.loading_page, container, true); 89 mLoadingView = v.findViewById(R.id.loading_container); 90 return super.onCreateView(inflater, container, savedInstanceState); 91 } 92 93 @Override onActivityCreated(final Bundle savedInstanceState)94 public void onActivityCreated(final Bundle savedInstanceState) { 95 super.onActivityCreated(savedInstanceState); 96 final Activity activity = getActivity(); 97 mClientId = activity.getIntent().getStringExtra(DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT); 98 mConnectivityManager = 99 (ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE); 100 addPreferencesFromResource(R.xml.dictionary_settings); 101 refreshInterface(); 102 setHasOptionsMenu(true); 103 } 104 105 @Override onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)106 public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { 107 mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now); 108 mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 109 refreshNetworkState(); 110 } 111 112 @Override onResume()113 public void onResume() { 114 super.onResume(); 115 mChangedSettings = false; 116 UpdateHandler.registerUpdateEventListener(this); 117 final Activity activity = getActivity(); 118 if (!MetadataDbHelper.isClientKnown(activity, mClientId)) { 119 Log.i(TAG, "Unknown dictionary pack client: " + mClientId + ". Requesting info."); 120 final Intent unknownClientBroadcast = 121 new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT); 122 unknownClientBroadcast.putExtra( 123 DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId); 124 activity.sendBroadcast(unknownClientBroadcast); 125 } 126 final IntentFilter filter = new IntentFilter(); 127 filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); 128 getActivity().registerReceiver(mConnectivityChangedReceiver, filter); 129 refreshNetworkState(); 130 } 131 132 @Override onPause()133 public void onPause() { 134 super.onPause(); 135 final Activity activity = getActivity(); 136 UpdateHandler.unregisterUpdateEventListener(this); 137 activity.unregisterReceiver(mConnectivityChangedReceiver); 138 if (mChangedSettings) { 139 final Intent newDictBroadcast = 140 new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); 141 activity.sendBroadcast(newDictBroadcast); 142 mChangedSettings = false; 143 } 144 } 145 146 @Override downloadedMetadata(final boolean succeeded)147 public void downloadedMetadata(final boolean succeeded) { 148 stopLoadingAnimation(); 149 if (!succeeded) return; // If the download failed nothing changed, so no need to refresh 150 new Thread("refreshInterface") { 151 @Override 152 public void run() { 153 refreshInterface(); 154 } 155 }.start(); 156 } 157 158 @Override wordListDownloadFinished(final String wordListId, final boolean succeeded)159 public void wordListDownloadFinished(final String wordListId, final boolean succeeded) { 160 final WordListPreference pref = findWordListPreference(wordListId); 161 if (null == pref) return; 162 // TODO: Report to the user if !succeeded 163 final Activity activity = getActivity(); 164 if (null == activity) return; 165 activity.runOnUiThread(new Runnable() { 166 @Override 167 public void run() { 168 // We have to re-read the db in case the description has changed, and to 169 // find out what state it ended up if the download wasn't successful 170 // TODO: don't redo everything, only re-read and set this word list status 171 refreshInterface(); 172 } 173 }); 174 } 175 findWordListPreference(final String id)176 private WordListPreference findWordListPreference(final String id) { 177 final PreferenceGroup prefScreen = getPreferenceScreen(); 178 if (null == prefScreen) { 179 Log.e(TAG, "Could not find the preference group"); 180 return null; 181 } 182 for (int i = prefScreen.getPreferenceCount() - 1; i >= 0; --i) { 183 final Preference pref = prefScreen.getPreference(i); 184 if (pref instanceof WordListPreference) { 185 final WordListPreference wlPref = (WordListPreference)pref; 186 if (id.equals(wlPref.mWordlistId)) { 187 return wlPref; 188 } 189 } 190 } 191 Log.e(TAG, "Could not find the preference for a word list id " + id); 192 return null; 193 } 194 195 @Override updateCycleCompleted()196 public void updateCycleCompleted() {} 197 refreshNetworkState()198 private void refreshNetworkState() { 199 NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); 200 boolean isConnected = null == info ? false : info.isConnected(); 201 if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(isConnected); 202 } 203 refreshInterface()204 private void refreshInterface() { 205 final Activity activity = getActivity(); 206 if (null == activity) return; 207 final long lastUpdateDate = 208 MetadataDbHelper.getLastUpdateDateForClient(getActivity(), mClientId); 209 final PreferenceGroup prefScreen = getPreferenceScreen(); 210 final Collection<? extends Preference> prefList = 211 createInstalledDictSettingsCollection(mClientId); 212 213 final String updateNowSummary = getString(R.string.last_update) + " " 214 + DateUtils.formatDateTime(activity, lastUpdateDate, 215 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 216 217 activity.runOnUiThread(new Runnable() { 218 @Override 219 public void run() { 220 // TODO: display this somewhere 221 // if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary); 222 refreshNetworkState(); 223 224 removeAnyDictSettings(prefScreen); 225 for (Preference preference : prefList) { 226 prefScreen.addPreference(preference); 227 } 228 } 229 }); 230 } 231 createErrorMessage(final Activity activity, final int messageResource)232 private Preference createErrorMessage(final Activity activity, final int messageResource) { 233 final Preference message = new Preference(activity); 234 message.setTitle(messageResource); 235 message.setEnabled(false); 236 return message; 237 } 238 removeAnyDictSettings(final PreferenceGroup prefGroup)239 private void removeAnyDictSettings(final PreferenceGroup prefGroup) { 240 for (int i = prefGroup.getPreferenceCount() - 1; i >= 0; --i) { 241 prefGroup.removePreference(prefGroup.getPreference(i)); 242 } 243 } 244 245 /** 246 * Creates a WordListPreference list to be added to the screen. 247 * 248 * This method only creates the preferences but does not add them. 249 * Thus, it can be called on another thread. 250 * 251 * @param clientId the id of the client for which we want to display the dictionary list 252 * @return A collection of preferences ready to add to the interface. 253 */ createInstalledDictSettingsCollection( final String clientId)254 private Collection<? extends Preference> createInstalledDictSettingsCollection( 255 final String clientId) { 256 // This will directly contact the DictionaryProvider and request the list exactly like 257 // any regular client would do. 258 // Considering the respective value of the respective constants used here for each path, 259 // segment, the url generated by this is of the form (assuming "clientId" as a clientId) 260 // content://com.android.inputmethod.latin.dictionarypack/clientId/list?procotol=2 261 final Uri contentUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 262 .authority(getString(R.string.authority)) 263 .appendPath(clientId) 264 .appendPath(DICT_LIST_ID) 265 // Need to use version 2 to get this client's list 266 .appendQueryParameter(DictionaryProvider.QUERY_PARAMETER_PROTOCOL_VERSION, "2") 267 .build(); 268 final Activity activity = getActivity(); 269 final Cursor cursor = null == activity ? null 270 : activity.getContentResolver().query(contentUri, null, null, null, null); 271 272 if (null == cursor) { 273 final ArrayList<Preference> result = new ArrayList<Preference>(); 274 result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service)); 275 return result; 276 } else if (!cursor.moveToFirst()) { 277 final ArrayList<Preference> result = new ArrayList<Preference>(); 278 result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); 279 cursor.close(); 280 return result; 281 } else { 282 final String systemLocaleString = Locale.getDefault().toString(); 283 final TreeMap<String, WordListPreference> prefMap = 284 new TreeMap<String, WordListPreference>(); 285 final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); 286 final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); 287 final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); 288 final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); 289 final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); 290 final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); 291 do { 292 final String wordlistId = cursor.getString(idIndex); 293 final int version = cursor.getInt(versionIndex); 294 final String localeString = cursor.getString(localeIndex); 295 final Locale locale = new Locale(localeString); 296 final String description = cursor.getString(descriptionIndex); 297 final int status = cursor.getInt(statusIndex); 298 final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString); 299 final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel); 300 final int filesize = cursor.getInt(filesizeIndex); 301 // The key is sorted in lexicographic order, according to the match level, then 302 // the description. 303 final String key = matchLevelString + "." + description + "." + wordlistId; 304 final WordListPreference existingPref = prefMap.get(key); 305 if (null == existingPref || hasPriority(status, existingPref.mStatus)) { 306 final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); 307 final WordListPreference pref; 308 if (null != oldPreference 309 && oldPreference.mVersion == version 310 && oldPreference.mLocale.equals(locale)) { 311 // If the old preference has all the new attributes, reuse it. We test 312 // for version and locale because although attributes other than status 313 // need to be the same, others have been tested through the key of the 314 // map. Also, status may differ so we don't want to use #equals() here. 315 pref = oldPreference; 316 pref.mStatus = status; 317 } else { 318 // Otherwise, discard it and create a new one instead. 319 pref = new WordListPreference(activity, mDictionaryListInterfaceState, 320 mClientId, wordlistId, version, locale, description, status, 321 filesize); 322 } 323 prefMap.put(key, pref); 324 } 325 } while (cursor.moveToNext()); 326 cursor.close(); 327 mCurrentPreferenceMap = prefMap; 328 return prefMap.values(); 329 } 330 } 331 332 /** 333 * Finds out if a given status has priority over another for display order. 334 * 335 * @param newStatus 336 * @param oldStatus 337 * @return whether newStatus has priority over oldStatus. 338 */ hasPriority(final int newStatus, final int oldStatus)339 private static boolean hasPriority(final int newStatus, final int oldStatus) { 340 // Both of these should be one of MetadataDbHelper.STATUS_* 341 return newStatus > oldStatus; 342 } 343 344 @Override onOptionsItemSelected(final MenuItem item)345 public boolean onOptionsItemSelected(final MenuItem item) { 346 switch (item.getItemId()) { 347 case MENU_UPDATE_NOW: 348 if (View.GONE == mLoadingView.getVisibility()) { 349 startRefresh(); 350 } else { 351 cancelRefresh(); 352 } 353 return true; 354 } 355 return false; 356 } 357 startRefresh()358 private void startRefresh() { 359 startLoadingAnimation(); 360 mChangedSettings = true; 361 UpdateHandler.registerUpdateEventListener(this); 362 final Activity activity = getActivity(); 363 new Thread("updateByHand") { 364 @Override 365 public void run() { 366 UpdateHandler.update(activity, true); 367 } 368 }.start(); 369 } 370 cancelRefresh()371 private void cancelRefresh() { 372 UpdateHandler.unregisterUpdateEventListener(this); 373 final Context context = getActivity(); 374 UpdateHandler.cancelUpdate(context, mClientId); 375 stopLoadingAnimation(); 376 } 377 startLoadingAnimation()378 private void startLoadingAnimation() { 379 mLoadingView.setVisibility(View.VISIBLE); 380 getView().setVisibility(View.GONE); 381 mUpdateNowMenu.setTitle(R.string.cancel); 382 } 383 stopLoadingAnimation()384 private void stopLoadingAnimation() { 385 final View preferenceView = getView(); 386 final Activity activity = getActivity(); 387 if (null == activity) return; 388 activity.runOnUiThread(new Runnable() { 389 @Override 390 public void run() { 391 mLoadingView.setVisibility(View.GONE); 392 preferenceView.setVisibility(View.VISIBLE); 393 mLoadingView.startAnimation(AnimationUtils.loadAnimation( 394 getActivity(), android.R.anim.fade_out)); 395 preferenceView.startAnimation(AnimationUtils.loadAnimation( 396 getActivity(), android.R.anim.fade_in)); 397 // The menu is created by the framework asynchronously after the activity, 398 // which means it's possible to have the activity running but the menu not 399 // created yet - hence the necessity for a null check here. 400 if (null != mUpdateNowMenu) { 401 mUpdateNowMenu.setTitle(R.string.check_for_updates_now); 402 } 403 } 404 }); 405 } 406 } 407