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