aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
blob: 14e0050078c0c942c42c5d3c5784c0e13bf67ad6 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
/**
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.android.inputmethod.dictionarypack;

import com.android.inputmethod.latin.common.LocaleUtils;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;

import com.android.inputmethod.latin.R;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import java.util.TreeMap;

/**
 * Preference screen.
 */
public final class DictionarySettingsFragment extends PreferenceFragment
        implements UpdateHandler.UpdateEventListener {
    private static final String TAG = DictionarySettingsFragment.class.getSimpleName();

    static final private String DICT_LIST_ID = "list";
    static final public String DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT = "clientId";

    static final private int MENU_UPDATE_NOW = Menu.FIRST;

    private View mLoadingView;
    private String mClientId;
    private ConnectivityManager mConnectivityManager;
    private MenuItem mUpdateNowMenu;
    private boolean mChangedSettings;
    private DictionaryListInterfaceState mDictionaryListInterfaceState =
            new DictionaryListInterfaceState();
    // never null
    private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>();

    private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(final Context context, final Intent intent) {
                refreshNetworkState();
            }
        };

    /**
     * Empty constructor for fragment generation.
     */
    public DictionarySettingsFragment() {
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
            final Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.loading_page, container, true);
        mLoadingView = v.findViewById(R.id.loading_container);
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onActivityCreated(final Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final Activity activity = getActivity();
        mClientId = activity.getIntent().getStringExtra(DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT);
        mConnectivityManager =
                (ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE);
        addPreferencesFromResource(R.xml.dictionary_settings);
        refreshInterface();
        setHasOptionsMenu(true);
    }

    @Override
    public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
        final String metadataUri =
                MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId);
        // We only add the "Refresh" button if we have a non-empty URL to refresh from. If the
        // URL is empty, of course we can't refresh so it makes no sense to display this.
        if (!TextUtils.isEmpty(metadataUri)) {
            mUpdateNowMenu =
                    menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now);
            mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
            refreshNetworkState();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        mChangedSettings = false;
        UpdateHandler.registerUpdateEventListener(this);
        final Activity activity = getActivity();
        if (!MetadataDbHelper.isClientKnown(activity, mClientId)) {
            Log.i(TAG, "Unknown dictionary pack client: " + mClientId + ". Requesting info.");
            final Intent unknownClientBroadcast =
                    new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT);
            unknownClientBroadcast.putExtra(
                    DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId);
            activity.sendBroadcast(unknownClientBroadcast);
        }
        final IntentFilter filter = new IntentFilter();
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        getActivity().registerReceiver(mConnectivityChangedReceiver, filter);
        refreshNetworkState();
    }

    @Override
    public void onPause() {
        super.onPause();
        final Activity activity = getActivity();
        UpdateHandler.unregisterUpdateEventListener(this);
        activity.unregisterReceiver(mConnectivityChangedReceiver);
        if (mChangedSettings) {
            final Intent newDictBroadcast =
                    new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
            activity.sendBroadcast(newDictBroadcast);
            mChangedSettings = false;
        }
    }

    @Override
    public void downloadedMetadata(final boolean succeeded) {
        stopLoadingAnimation();
        if (!succeeded) return; // If the download failed nothing changed, so no need to refresh
        new Thread("refreshInterface") {
            @Override
            public void run() {
                refreshInterface();
            }
        }.start();
    }

    @Override
    public void wordListDownloadFinished(final String wordListId, final boolean succeeded) {
        final WordListPreference pref = findWordListPreference(wordListId);
        if (null == pref) return;
        // TODO: Report to the user if !succeeded
        final Activity activity = getActivity();
        if (null == activity) return;
        activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // We have to re-read the db in case the description has changed, and to
                    // find out what state it ended up if the download wasn't successful
                    // TODO: don't redo everything, only re-read and set this word list status
                    refreshInterface();
                }
            });
    }

    private WordListPreference findWordListPreference(final String id) {
        final PreferenceGroup prefScreen = getPreferenceScreen();
        if (null == prefScreen) {
            Log.e(TAG, "Could not find the preference group");
            return null;
        }
        for (int i = prefScreen.getPreferenceCount() - 1; i >= 0; --i) {
            final Preference pref = prefScreen.getPreference(i);
            if (pref instanceof WordListPreference) {
                final WordListPreference wlPref = (WordListPreference)pref;
                if (id.equals(wlPref.mWordlistId)) {
                    return wlPref;
                }
            }
        }
        Log.e(TAG, "Could not find the preference for a word list id " + id);
        return null;
    }

    @Override
    public void updateCycleCompleted() {}

    void refreshNetworkState() {
        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
        boolean isConnected = null == info ? false : info.isConnected();
        if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(isConnected);
    }

    void refreshInterface() {
        final Activity activity = getActivity();
        if (null == activity) return;
        final PreferenceGroup prefScreen = getPreferenceScreen();
        final Collection<? extends Preference> prefList =
                createInstalledDictSettingsCollection(mClientId);

        activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // TODO: display this somewhere
                    // if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
                    refreshNetworkState();

                    removeAnyDictSettings(prefScreen);
                    int i = 0;
                    for (Preference preference : prefList) {
                        preference.setOrder(i++);
                        prefScreen.addPreference(preference);
                    }
                }
            });
    }

    private static Preference createErrorMessage(final Activity activity, final int messageResource) {
        final Preference message = new Preference(activity);
        message.setTitle(messageResource);
        message.setEnabled(false);
        return message;
    }

    static void removeAnyDictSettings(final PreferenceGroup prefGroup) {
        for (int i = prefGroup.getPreferenceCount() - 1; i >= 0; --i) {
            prefGroup.removePreference(prefGroup.getPreference(i));
        }
    }

    /**
     * Creates a WordListPreference list to be added to the screen.
     *
     * This method only creates the preferences but does not add them.
     * Thus, it can be called on another thread.
     *
     * @param clientId the id of the client for which we want to display the dictionary list
     * @return A collection of preferences ready to add to the interface.
     */
    private Collection<? extends Preference> createInstalledDictSettingsCollection(
            final String clientId) {
        // This will directly contact the DictionaryProvider and request the list exactly like
        // any regular client would do.
        // Considering the respective value of the respective constants used here for each path,
        // segment, the url generated by this is of the form (assuming "clientId" as a clientId)
        // content://com.android.inputmethod.latin.dictionarypack/clientId/list?procotol=2
        final Uri contentUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                .authority(getString(R.string.authority))
                .appendPath(clientId)
                .appendPath(DICT_LIST_ID)
                // Need to use version 2 to get this client's list
                .appendQueryParameter(DictionaryProvider.QUERY_PARAMETER_PROTOCOL_VERSION, "2")
                .build();
        final Activity activity = getActivity();
        final Cursor cursor = (null == activity) ? null
                : activity.getContentResolver().query(contentUri, null, null, null, null);

        if (null == cursor) {
            final ArrayList<Preference> result = new ArrayList<>();
            result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service));
            return result;
        }
        try {
            if (!cursor.moveToFirst()) {
                final ArrayList<Preference> result = new ArrayList<>();
                result.add(createErrorMessage(activity, R.string.no_dictionaries_available));
                return result;
            }
            final String systemLocaleString = Locale.getDefault().toString();
            final TreeMap<String, WordListPreference> prefMap = new TreeMap<>();
            final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
            final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
            final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
            final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN);
            final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
            final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN);
            do {
                final String wordlistId = cursor.getString(idIndex);
                final int version = cursor.getInt(versionIndex);
                final String localeString = cursor.getString(localeIndex);
                final Locale locale = new Locale(localeString);
                final String description = cursor.getString(descriptionIndex);
                final int status = cursor.getInt(statusIndex);
                final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString);
                final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel);
                final int filesize = cursor.getInt(filesizeIndex);
                // The key is sorted in lexicographic order, according to the match level, then
                // the description.
                final String key = matchLevelString + "." + description + "." + wordlistId;
                final WordListPreference existingPref = prefMap.get(key);
                if (null == existingPref || existingPref.hasPriorityOver(status)) {
                    final WordListPreference oldPreference = mCurrentPreferenceMap.get(key);
                    final WordListPreference pref;
                    if (null != oldPreference
                            && oldPreference.mVersion == version
                            && oldPreference.hasStatus(status)
                            && oldPreference.mLocale.equals(locale)) {
                        // If the old preference has all the new attributes, reuse it. Ideally,
                        // we should reuse the old pref even if its status is different and call
                        // setStatus here, but setStatus calls Preference#setSummary() which
                        // needs to be done on the UI thread and we're not on the UI thread
                        // here. We could do all this work on the UI thread, but in this case
                        // it's probably lighter to stay on a background thread and throw this
                        // old preference out.
                        pref = oldPreference;
                    } else {
                        // Otherwise, discard it and create a new one instead.
                        // TODO: when the status is different from the old one, we need to
                        // animate the old one out before animating the new one in.
                        pref = new WordListPreference(activity, mDictionaryListInterfaceState,
                                mClientId, wordlistId, version, locale, description, status,
                                filesize);
                    }
                    prefMap.put(key, pref);
                }
            } while (cursor.moveToNext());
            mCurrentPreferenceMap = prefMap;
            return prefMap.values();
        } finally {
            cursor.close();
        }
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
        case MENU_UPDATE_NOW:
            if (View.GONE == mLoadingView.getVisibility()) {
                startRefresh();
            } else {
                cancelRefresh();
            }
            return true;
        }
        return false;
    }

    private void startRefresh() {
        startLoadingAnimation();
        mChangedSettings = true;
        UpdateHandler.registerUpdateEventListener(this);
        final Activity activity = getActivity();
        new Thread("updateByHand") {
            @Override
            public void run() {
                // We call tryUpdate(), which returns whether we could successfully start an update.
                // If we couldn't, we'll never receive the end callback, so we stop the loading
                // animation and return to the previous screen.
                if (!UpdateHandler.tryUpdate(activity, true)) {
                    stopLoadingAnimation();
                }
            }
        }.start();
    }

    private void cancelRefresh() {
        UpdateHandler.unregisterUpdateEventListener(this);
        final Context context = getActivity();
        UpdateHandler.cancelUpdate(context, mClientId);
        stopLoadingAnimation();
    }

    private void startLoadingAnimation() {
        mLoadingView.setVisibility(View.VISIBLE);
        getView().setVisibility(View.GONE);
        // We come here when the menu element is pressed so presumably it can't be null. But
        // better safe than sorry.
        if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel);
    }

    void stopLoadingAnimation() {
        final View preferenceView = getView();
        final Activity activity = getActivity();
        if (null == activity) return;
        final View loadingView = mLoadingView;
        final MenuItem updateNowMenu = mUpdateNowMenu;
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                loadingView.setVisibility(View.GONE);
                preferenceView.setVisibility(View.VISIBLE);
                loadingView.startAnimation(AnimationUtils.loadAnimation(
                        activity, android.R.anim.fade_out));
                preferenceView.startAnimation(AnimationUtils.loadAnimation(
                        activity, android.R.anim.fade_in));
                // The menu is created by the framework asynchronously after the activity,
                // which means it's possible to have the activity running but the menu not
                // created yet - hence the necessity for a null check here.
                if (null != updateNowMenu) {
                    updateNowMenu.setTitle(R.string.check_for_updates_now);
                }
            }
        });
    }
}