Home | History | Annotate | Download | only in dictionarypack
      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      */
     85     public DictionarySettingsFragment() {
     86     }
     87 
     88     @Override
     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
     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
    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
    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
    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
    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
    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 
    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
    224     public void updateCycleCompleted() {}
    225 
    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 
    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 
    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 
    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      */
    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
    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 
    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 
    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 
    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 
    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