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