10cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard/*
20cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * Copyright (C) 2011 The Android Open Source Project
30cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard *
40cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * Licensed under the Apache License, Version 2.0 (the "License"); you may not
50cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * use this file except in compliance with the License. You may obtain a copy of
60cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * the License at
70cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard *
80cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * http://www.apache.org/licenses/LICENSE-2.0
90cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard *
100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * Unless required by applicable law or agreed to in writing, software
110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * License for the specific language governing permissions and limitations under
140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * the License.
150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard */
160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardpackage com.android.inputmethod.dictionarypack;
180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.app.DownloadManager;
200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.app.DownloadManager.Query;
210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.app.DownloadManager.Request;
220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.app.Notification;
230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.app.NotificationManager;
240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.app.PendingIntent;
250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.content.ContentValues;
260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.content.Context;
270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.content.Intent;
280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.content.SharedPreferences;
290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.content.res.Resources;
300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.database.Cursor;
310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.database.sqlite.SQLiteDatabase;
320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.net.ConnectivityManager;
330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.net.Uri;
34a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalardimport android.os.Build;
350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.os.ParcelFileDescriptor;
360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.text.TextUtils;
370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport android.util.Log;
380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport com.android.inputmethod.compat.ConnectivityManagerCompatUtils;
400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport com.android.inputmethod.compat.DownloadManagerCompatUtils;
41a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalardimport com.android.inputmethod.compat.NotificationCompatUtils;
420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport com.android.inputmethod.latin.R;
434be6198cb73cc24e10834153c4e049644ed187e3Tadashi G. Takaokaimport com.android.inputmethod.latin.utils.ApplicationUtils;
4403118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasaimport com.android.inputmethod.latin.utils.DebugLogUtils;
450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.File;
470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.FileInputStream;
480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.FileNotFoundException;
490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.FileOutputStream;
500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.IOException;
510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.InputStream;
520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.InputStreamReader;
530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.io.OutputStream;
540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.nio.channels.FileChannel;
550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.ArrayList;
560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.Collections;
570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.LinkedList;
580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.List;
590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.Locale;
600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.Set;
610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardimport java.util.TreeSet;
620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard/**
640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * Handler for the update process.
650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard *
660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * This class is in charge of coordinating the update process for the various dictionaries
670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard * stored in the dictionary pack.
680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard */
690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalardpublic final class UpdateHandler {
700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    static final String TAG = "DictionaryProvider:" + UpdateHandler.class.getSimpleName();
710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static final boolean DEBUG = DictionaryProvider.DEBUG;
720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // Used to prevent trying to read the id of the downloaded file before it is written
740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    static final Object sSharedIdProtector = new Object();
750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // Value used to mean this is not a real DownloadManager downloaded file id
770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column
780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // in SQLite, so it should never return anything < 0.
790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static final int NOT_AN_ID = -1;
800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = 2;
810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long.
830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static final int FILE_COPY_BUFFER_SIZE = 8192;
840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // Table fixed values for metadata / downloads
860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    final static String METADATA_NAME = "metadata";
870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    final static int METADATA_TYPE = 0;
880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    final static int WORDLIST_TYPE = 1;
890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // Suffix for generated dictionary files
910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static final String DICT_FILE_SUFFIX = ".dict";
920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // Name of the category for the main dictionary
930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static final String MAIN_DICTIONARY_CATEGORY = "main";
940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // The id for the "dictionary available" notification.
960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    static final int DICT_AVAILABLE_NOTIFICATION_ID = 1;
970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * An interface for UIs or services that want to know when something happened.
1000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
1010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This is chiefly used by the dictionary manager UI.
1020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public interface UpdateEventListener {
1040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        public void downloadedMetadata(boolean succeeded);
1050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        public void wordListDownloadFinished(String wordListId, boolean succeeded);
1060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        public void updateCycleCompleted();
1070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
1080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * The list of currently registered listeners.
1110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static List<UpdateEventListener> sUpdateEventListeners
1130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            = Collections.synchronizedList(new LinkedList<UpdateEventListener>());
1140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Register a new listener to be notified of updates.
1170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
1180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Don't forget to call unregisterUpdateEventListener when done with it, or
1190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * it will leak the register.
1200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void registerUpdateEventListener(final UpdateEventListener listener) {
1220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        sUpdateEventListeners.add(listener);
1230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
1240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Unregister a previously registered listener.
1270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void unregisterUpdateEventListener(final UpdateEventListener listener) {
1290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        sUpdateEventListeners.remove(listener);
1300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
1310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static final String DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY = "downloadOverMetered";
1330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Write the DownloadManager ID of the currently downloading metadata to permanent storage.
1360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
1370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context to open shared prefs
1380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param uri the uri of the metadata
1390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param downloadId the id returned by DownloadManager
1400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void writeMetadataDownloadId(final Context context, final String uri,
1420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final long downloadId) {
1430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        MetadataDbHelper.registerMetadataDownloadId(context, uri, downloadId);
1440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
1450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static final int DOWNLOAD_OVER_METERED_SETTING_UNKNOWN = 0;
1470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static final int DOWNLOAD_OVER_METERED_ALLOWED = 1;
1480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static final int DOWNLOAD_OVER_METERED_DISALLOWED = 2;
1490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Sets the setting that tells us whether we may download over a metered connection.
1520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void setDownloadOverMeteredSetting(final Context context,
1540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final boolean shouldDownloadOverMetered) {
1550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
1560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final SharedPreferences.Editor editor = prefs.edit();
1570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        editor.putInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, shouldDownloadOverMetered
1580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                ? DOWNLOAD_OVER_METERED_ALLOWED : DOWNLOAD_OVER_METERED_DISALLOWED);
1590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        editor.apply();
1600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
1610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Gets the setting that tells us whether we may download over a metered connection.
1640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
1650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This returns one of the constants above.
1660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static int getDownloadOverMeteredSetting(final Context context) {
1680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
1690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final int setting = prefs.getInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY,
1700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                DOWNLOAD_OVER_METERED_SETTING_UNKNOWN);
1710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        return setting;
1720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
1730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
1740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
1750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Download latest metadata from the server through DownloadManager for all known clients
1760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context The context for retrieving resources
1770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param updateNow Whether we should update NOW, or respect bandwidth policies
1783f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard     * @return true if an update successfully started, false otherwise.
1790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
1803f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard    public static boolean tryUpdate(final Context context, final boolean updateNow) {
1810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: loop through all clients instead of only doing the default one.
182a91561aa58db1c43092c1caecc051a11fa5391c7Tadashi G. Takaoka        final TreeSet<String> uris = new TreeSet<>();
1830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final Cursor cursor = MetadataDbHelper.queryClientIds(context);
1843f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard        if (null == cursor) return false;
1850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        try {
1863f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard            if (!cursor.moveToFirst()) return false;
1870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            do {
1880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final String clientId = cursor.getString(0);
1890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final String metadataUri =
1900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        MetadataDbHelper.getMetadataUriAsString(context, clientId);
19103118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa                PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId));
19203118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa                DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
1930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                uris.add(metadataUri);
1940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } while (cursor.moveToNext());
1950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } finally {
1960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            cursor.close();
1970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
1983f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard        boolean started = false;
1990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (final String metadataUri : uris) {
2000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (!TextUtils.isEmpty(metadataUri)) {
2010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // If the metadata URI is empty, that means we should never update it at all.
2020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // It should not be possible to come here with a null metadata URI, because
2030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // it should have been rejected at the time of client registration; if there
2040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // is a bug and it happens anyway, doing nothing is the right thing to do.
2050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}.
2060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                updateClientsWithMetadataUri(context, updateNow, metadataUri);
2073f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard                started = true;
2080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
2090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
2103f0858eb2bcb9414dd94e01991b02c785af7b871Jean Chalard        return started;
2110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
2120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
2130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
2140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Download latest metadata from the server through DownloadManager for all relevant clients
2150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
2160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context The context for retrieving resources
2170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param updateNow Whether we should update NOW, or respect bandwidth policies
2180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param metadataUri The client to update
2190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
2200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void updateClientsWithMetadataUri(final Context context,
2210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final boolean updateNow, final String metadataUri) {
22203118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        PrivateLog.log("Update for metadata URI " + DebugLogUtils.s(metadataUri));
22343590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard        // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
22443590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard        // DownloadManager also stupidly cuts the extension to replace with its own that it
22543590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard        // gets from the content-type. We need to circumvent this.
22643590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard        final String disambiguator = "#" + System.currentTimeMillis()
2274be6198cb73cc24e10834153c4e049644ed187e3Tadashi G. Takaoka                + ApplicationUtils.getVersionName(context) + ".json";
22843590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard        final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
22903118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Request =", metadataRequest);
2300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
2310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final Resources res = context.getResources();
2320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // By default, download over roaming is allowed and all network types are allowed too.
2330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (!updateNow) {
2340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final boolean allowedOverMetered = res.getBoolean(R.bool.allow_over_metered);
2350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // If we don't have to update NOW, then only do it over non-metered connections.
2360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (DownloadManagerCompatUtils.hasSetAllowedOverMetered()) {
2370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                DownloadManagerCompatUtils.setAllowedOverMetered(metadataRequest,
2380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        allowedOverMetered);
2390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else if (!allowedOverMetered) {
2400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI);
2410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
2420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            metadataRequest.setAllowedOverRoaming(res.getBoolean(R.bool.allow_over_roaming));
2430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
2440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final boolean notificationVisible = updateNow
2450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                ? res.getBoolean(R.bool.display_notification_for_user_requested_update)
2460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                : res.getBoolean(R.bool.display_notification_for_auto_update);
2470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
2480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        metadataRequest.setTitle(res.getString(R.string.download_description));
2490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        metadataRequest.setNotificationVisibility(notificationVisible
2500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                ? Request.VISIBILITY_VISIBLE : Request.VISIBILITY_HIDDEN);
2510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        metadataRequest.setVisibleInDownloadsUi(
2520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI));
2530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
254a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard        final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
2550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        cancelUpdateWithDownloadManager(context, metadataUri, manager);
2560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final long downloadId;
2570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        synchronized (sSharedIdProtector) {
2580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            downloadId = manager.enqueue(metadataRequest);
25903118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa            DebugLogUtils.l("Metadata download requested with id", downloadId);
2600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // If there is already a download in progress, it's been there for a while and
2610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // there is probably something wrong with download manager. It's best to just
2620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // overwrite the id and request it again. If the old one happens to finish
2630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // anyway, we don't know about its ID any more, so the downloadFinished
2640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // method will ignore it.
2650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            writeMetadataDownloadId(context, metadataUri, downloadId);
2660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
267f8014eea341040f8d155e071e4e0c915a7ebd61dJean Chalard        PrivateLog.log("Requested download with id " + downloadId);
2680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
2690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
2700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
271faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard     * Cancels downloading a file, if there is one for this URI.
2720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
273faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard     * If we are not currently downloading the file at this URI, this is a no-op.
2740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
2750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to open the database on
276faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard     * @param metadataUri the URI to cancel
277a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard     * @param manager an wrapped instance of DownloadManager
2780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
2790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void cancelUpdateWithDownloadManager(final Context context,
280a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard            final String metadataUri, final DownloadManagerWrapper manager) {
2810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        synchronized (sSharedIdProtector) {
2820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final long metadataDownloadId =
283faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard                    MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri);
2840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (NOT_AN_ID == metadataDownloadId) return;
2850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            manager.remove(metadataDownloadId);
286faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard            writeMetadataDownloadId(context, metadataUri, NOT_AN_ID);
2870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
2880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Consider a cancellation as a failure. As such, inform listeners that the download
2890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // has failed.
2900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
2910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            listener.downloadedMetadata(false);
2920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
2930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
2940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
2950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
296faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard     * Cancels a pending update for this client, if there is one.
2970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
298faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard     * If we are not currently updating metadata for this client, this is a no-op. This is a helper
299faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard     * method that gets the download manager service and the metadata URI for this client.
3000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
3010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context, to get an instance of DownloadManager
3020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the ID of the client we want to cancel the update of
3030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
3040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void cancelUpdate(final Context context, final String clientId) {
305a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard        final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
306faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard        final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId);
307a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard        cancelUpdateWithDownloadManager(context, metadataUri, manager);
3080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
3090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
3100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
3110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Registers a download request and flags it as downloading in the metadata table.
3120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
3130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This is a helper method that exists to avoid race conditions where DownloadManager might
3140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * finish downloading the file before the data is committed to the database.
3150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * It registers the request with the DownloadManager service and also updates the metadata
3160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * database directly within a synchronized section.
3170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This method has no intelligence about the data it commits to the database aside from the
3180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * download request id, which is not known before submitting the request to the download
3190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * manager. Hence, it only updates the relevant line.
3200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
321a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard     * @param manager a wrapped download manager service to register the request with.
3220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param request the request to register.
3230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param db the metadata database.
3240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param id the id of the word list.
3250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param version the version of the word list.
3260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @return the download id returned by the download manager.
3270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
328a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard    public static long registerDownloadRequest(final DownloadManagerWrapper manager,
329a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard            final Request request, final SQLiteDatabase db, final String id, final int version) {
33003118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
3310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final long downloadId;
3320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        synchronized (sSharedIdProtector) {
3330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            downloadId = manager.enqueue(request);
33403118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa            DebugLogUtils.l("Download requested with id", downloadId);
3350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId);
3360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
3370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        return downloadId;
3380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
3390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
3400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
3410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Retrieve information about a specific download from DownloadManager.
3420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
343a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard    private static CompletedDownloadInfo getCompletedDownloadInfo(
344a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard            final DownloadManagerWrapper manager, final long downloadId) {
3450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final Query query = new Query().setFilterById(downloadId);
3460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final Cursor cursor = manager.query(query);
3470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
3480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == cursor) {
3490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return new CompletedDownloadInfo(null, downloadId, DownloadManager.STATUS_FAILED);
3500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
3510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        try {
3520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String uri;
3530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final int status;
3540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (cursor.moveToNext()) {
3550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final int columnStatus = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
3560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final int columnError = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
3570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI);
3580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final int error = cursor.getInt(columnError);
3590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                status = cursor.getInt(columnStatus);
36043590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                final String uriWithAnchor = cursor.getString(columnUri);
36143590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                int anchorIndex = uriWithAnchor.indexOf('#');
36243590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                if (anchorIndex != -1) {
36343590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                    uri = uriWithAnchor.substring(0, anchorIndex);
36443590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                } else {
36543590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                    uri = uriWithAnchor;
36643590149a5c2073a9fc8e3ed6afbf21fb017193eJean Chalard                }
3670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (DownloadManager.STATUS_SUCCESSFUL != status) {
3680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    Log.e(TAG, "Permanent failure of download " + downloadId
3690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            + " with error code: " + error);
3700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
3710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else {
3720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                uri = null;
3730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                status = DownloadManager.STATUS_FAILED;
3740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
3750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return new CompletedDownloadInfo(uri, downloadId, status);
3760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } finally {
3770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            cursor.close();
3780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
3790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
3800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
3810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static ArrayList<DownloadRecord> getDownloadRecordsForCompletedDownloadInfo(
3820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final Context context, final CompletedDownloadInfo downloadInfo) {
3830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Get and check the ID of the file we are waiting for, compare them to downloaded ones
3840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        synchronized(sSharedIdProtector) {
3850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final ArrayList<DownloadRecord> downloadRecords =
3860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    MetadataDbHelper.getDownloadRecordsForDownloadId(context,
3870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            downloadInfo.mDownloadId);
3880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // If any of these is metadata, we should update the DB
3890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            boolean hasMetadata = false;
3900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            for (DownloadRecord record : downloadRecords) {
3910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (null == record.mAttributes) {
3920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    hasMetadata = true;
3930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    break;
3940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
3950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
3960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (hasMetadata) {
3970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                writeMetadataDownloadId(context, downloadInfo.mUri, NOT_AN_ID);
3980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataDbHelper.saveLastUpdateTimeOfUri(context, downloadInfo.mUri);
3990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
4000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return downloadRecords;
4010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
4020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
4030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
4040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
4050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Take appropriate action after a download finished, in success or in error.
4060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
4070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This is called by the system upon broadcast from the DownloadManager that a file
4080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * has been downloaded successfully.
4090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * After a simple check that this is actually the file we are waiting for, this
4100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * method basically coordinates the parsing and comparison of metadata, and fires
4110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * the computation of the list of actions that should be taken then executes them.
4120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
4130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context The context for this action.
4140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param intent The intent from the DownloadManager containing details about the download.
4150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
4160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /* package */ static void downloadFinished(final Context context, final Intent intent) {
4170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Get and check the ID of the file that was downloaded
4180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID);
419f8014eea341040f8d155e071e4e0c915a7ebd61dJean Chalard        PrivateLog.log("Download finished with id " + fileId);
42003118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("DownloadFinished with id", fileId);
4210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
4220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
423a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard        final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
4240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId);
4250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
4260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ArrayList<DownloadRecord> recordList =
4270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo);
4280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == recordList) return; // It was someone else's download.
42903118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Received result for download ", fileId);
4300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
4310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: handle gracefully a null pointer here. This is practically impossible because
4320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // we come here only when DownloadManager explicitly called us when it ended a
4330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // download, so we are pretty sure it's alive. It's theoretically possible that it's
4340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // disabled right inbetween the firing of the intent and the control reaching here.
4350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
4360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (final DownloadRecord record : recordList) {
4370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // downloadSuccessful is not final because we may still have exceptions from now on
4380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            boolean downloadSuccessful = false;
4390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            try {
4400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (downloadInfo.wasSuccessful()) {
4410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    downloadSuccessful = handleDownloadedFile(context, record, manager, fileId);
4420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
4430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } finally {
4440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (record.isMetadata()) {
4450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    publishUpdateMetadataCompleted(context, downloadSuccessful);
4460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                } else {
4470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId);
4480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    publishUpdateWordListCompleted(context, downloadSuccessful, fileId,
4490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            db, record.mAttributes, record.mClientId);
4500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
4510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
4520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
4530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Now that we're done using it, we can remove this download from DLManager
4540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        manager.remove(fileId);
4550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
4560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
45776d5f512f99700a963aa20a02590833e37221bffJean Chalard    /**
45876d5f512f99700a963aa20a02590833e37221bffJean Chalard     * Sends a broadcast informing listeners that the dictionaries were updated.
45976d5f512f99700a963aa20a02590833e37221bffJean Chalard     *
46076d5f512f99700a963aa20a02590833e37221bffJean Chalard     * This will call all local listeners through the UpdateEventListener#downloadedMetadata
46176d5f512f99700a963aa20a02590833e37221bffJean Chalard     * callback (for example, the dictionary provider interface uses this to stop the Loading
46276d5f512f99700a963aa20a02590833e37221bffJean Chalard     * animation) and send a broadcast about the metadata having been updated. For a client of
46376d5f512f99700a963aa20a02590833e37221bffJean Chalard     * the dictionary pack like Latin IME, this means it should re-query the dictionary pack
46476d5f512f99700a963aa20a02590833e37221bffJean Chalard     * for any relevant new data.
46576d5f512f99700a963aa20a02590833e37221bffJean Chalard     *
46676d5f512f99700a963aa20a02590833e37221bffJean Chalard     * @param context the context, to send the broadcast.
46776d5f512f99700a963aa20a02590833e37221bffJean Chalard     * @param downloadSuccessful whether the download of the metadata was successful or not.
46876d5f512f99700a963aa20a02590833e37221bffJean Chalard     */
46976d5f512f99700a963aa20a02590833e37221bffJean Chalard    public static void publishUpdateMetadataCompleted(final Context context,
4700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final boolean downloadSuccessful) {
4710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // We need to warn all listeners of what happened. But some listeners may want to
4720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // remove themselves or re-register something in response. Hence we should take a
4730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // snapshot of the listener list and warn them all. This also prevents any
4740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // concurrent modification problem of the static list.
4750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
4760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            listener.downloadedMetadata(downloadSuccessful);
4770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
4780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        publishUpdateCycleCompletedEvent(context);
4790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
4800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
4810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void publishUpdateWordListCompleted(final Context context,
4820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final boolean downloadSuccessful, final long fileId,
4830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final SQLiteDatabase db, final ContentValues downloadedFileRecord,
4840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String clientId) {
4850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        synchronized(sSharedIdProtector) {
4860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (downloadSuccessful) {
4870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final ActionBatch actions = new ActionBatch();
4880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                actions.add(new ActionBatch.InstallAfterDownloadAction(clientId,
4890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        downloadedFileRecord));
4900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                actions.execute(context, new LogProblemReporter(TAG));
4910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else {
4920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataDbHelper.deleteDownloadingEntry(db, fileId);
4930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
4940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
4950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // See comment above about #linkedCopyOfLists
4960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
4970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            listener.wordListDownloadFinished(downloadedFileRecord.getAsString(
4980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            MetadataDbHelper.WORDLISTID_COLUMN), downloadSuccessful);
4990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
5000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        publishUpdateCycleCompletedEvent(context);
5010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
5020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
5030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void publishUpdateCycleCompletedEvent(final Context context) {
5040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Even if this is not successful, we have to publish the new state.
505f8014eea341040f8d155e071e4e0c915a7ebd61dJean Chalard        PrivateLog.log("Publishing update cycle completed event");
50603118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Publishing update cycle completed event");
5070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
5080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            listener.updateCycleCompleted();
5090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
5100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        signalNewDictionaryState(context);
5110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
5120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
5130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static boolean handleDownloadedFile(final Context context,
514a149731a6764f259b7d15e05a2f557a3bdd23aabJean Chalard            final DownloadRecord downloadRecord, final DownloadManagerWrapper manager,
5150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final long fileId) {
5160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        try {
5170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // {@link handleWordList(Context,InputStream,ContentValues)}.
5180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // Handle the downloaded file according to its type
5190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (downloadRecord.isMetadata()) {
52003118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa                DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
5210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // #handleMetadata() closes its InputStream argument
5220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream(
5230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        manager.openDownloadedFile(fileId)), downloadRecord.mClientId);
5240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else {
52503118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa                DebugLogUtils.l("Data D/L'd is a word list");
5260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final int wordListStatus = downloadRecord.mAttributes.getAsInteger(
5270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        MetadataDbHelper.STATUS_COLUMN);
5280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) {
5290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // #handleWordList() closes its InputStream argument
5300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    handleWordList(context, new ParcelFileDescriptor.AutoCloseInputStream(
5310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            manager.openDownloadedFile(fileId)), downloadRecord);
5320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                } else {
5330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    Log.e(TAG, "Spurious download ended. Maybe a cancelled download?");
5340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
5350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
5360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return true;
5370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } catch (FileNotFoundException e) {
5380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            Log.e(TAG, "A file was downloaded but it can't be opened", e);
5390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } catch (IOException e) {
5400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // Can't read the file... disk damage?
5410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            Log.e(TAG, "Can't read a file", e);
5420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // TODO: Check with UX how we should warn the user.
5430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } catch (IllegalStateException e) {
5440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // The format of the downloaded file is incorrect. We should maybe report upstream?
5450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            Log.e(TAG, "Incorrect data received", e);
5460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } catch (BadFormatException e) {
5470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // The format of the downloaded file is incorrect. We should maybe report upstream?
5480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            Log.e(TAG, "Incorrect data received", e);
5490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
5500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        return false;
5510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
5520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
5530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
5540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Returns a copy of the specified list, with all elements copied.
5550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
5560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This returns a linked list.
5570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
5580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static <T> List<T> linkedCopyOfList(final List<T> src) {
5590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Instantiation of a parameterized type is not possible in Java, so it's not possible to
5600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // return the same type of list that was passed - probably the same reason why Collections
5610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // does not do it. So we need to decide statically which concrete type to return.
562a91561aa58db1c43092c1caecc051a11fa5391c7Tadashi G. Takaoka        return new LinkedList<>(src);
5630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
5640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
5650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
5660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data.
5670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
5680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void signalNewDictionaryState(final Context context) {
5691061bfdb34bbcb63bf0046eec42313d264ac33faJean Chalard        final Intent newDictBroadcast =
5701061bfdb34bbcb63bf0046eec42313d264ac33faJean Chalard                new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
5710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        context.sendBroadcast(newDictBroadcast);
5720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
5730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
5740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
5750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Parse metadata and take appropriate action (that is, upgrade dictionaries).
5760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to read settings.
5770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param stream an input stream pointing to the downloaded data. May not be null.
5780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *  Will be closed upon finishing.
5790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the ID of the client to update
5800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws BadFormatException if the metadata is not in a known format.
5810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws IOException if the downloaded file can't be read from the disk
5820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
5830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void handleMetadata(final Context context, final InputStream stream,
5840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String clientId) throws IOException, BadFormatException {
58503118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Entering handleMetadata");
5860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final List<WordListMetadata> newMetadata;
5870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final InputStreamReader reader = new InputStreamReader(stream);
5880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        try {
5890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // According to the doc InputStreamReader buffers, so no need to add a buffering layer
5900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            newMetadata = MetadataHandler.readMetadata(reader);
5910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } finally {
5920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            reader.close();
5930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
5940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
59503118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Downloaded metadata :", newMetadata);
596f8014eea341040f8d155e071e4e0c915a7ebd61dJean Chalard        PrivateLog.log("Downloaded metadata\n" + newMetadata);
5970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
5980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata);
5990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: Check with UX how we should report to the user
6000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: add an action to close the database
6010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.execute(context, new LogProblemReporter(TAG));
6020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
6030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
6050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Handle a word list: put it in its right place, and update the passed content values.
6060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context for opening files.
6070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param inputStream an input stream pointing to the downloaded data. May not be null.
6080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *  Will be closed upon finishing.
6090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param downloadRecord the content values to fill the file name in.
6100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws IOException if files can't be read or written.
6110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws BadFormatException if the md5 checksum doesn't match the metadata.
6120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
6130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void handleWordList(final Context context,
6140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final InputStream inputStream, final DownloadRecord downloadRecord)
6150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            throws IOException, BadFormatException {
6160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // DownloadManager does not have the ability to put the file directly where we want
6180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // it, so we had it download to a temporary place. Now we move it. It will be deleted
6190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // automatically by DownloadManager.
62003118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
6210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
6220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        PrivateLog.log("Downloaded a new word list with description : "
6230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
624f8014eea341040f8d155e071e4e0c915a7ebd61dJean Chalard                + " for " + downloadRecord.mClientId);
6250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String locale =
6270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN);
6280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String destinationFile = getTempFileName(context, locale);
6290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        downloadRecord.mAttributes.put(MetadataDbHelper.LOCAL_FILENAME_COLUMN, destinationFile);
6300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        FileOutputStream outputStream = null;
6320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        try {
6330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            outputStream = context.openFileOutput(destinationFile, Context.MODE_PRIVATE);
6340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            copyFile(inputStream, outputStream);
6350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } finally {
6360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            inputStream.close();
6370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (outputStream != null) {
6380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                outputStream.close();
6390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
6400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
6410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: Consolidate this MD5 calculation with file copying above.
6430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // We need to reopen the file because the inputstream bytes have been consumed, and there
6440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // is nothing in InputStream to reopen or rewind the stream
6450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        FileInputStream copiedFile = null;
6460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String md5sum;
6470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        try {
6480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            copiedFile = context.openFileInput(destinationFile);
6490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            md5sum = MD5Calculator.checksum(copiedFile);
6500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } finally {
6510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (copiedFile != null) {
6520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                copiedFile.close();
6530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
6540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
6550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (TextUtils.isEmpty(md5sum)) {
6560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return; // We can't compute the checksum anyway, so return and hope for the best
6570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
6580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (!md5sum.equals(downloadRecord.mAttributes.getAsString(
6590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataDbHelper.CHECKSUM_COLUMN))) {
6600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            context.deleteFile(destinationFile);
6610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            throw new BadFormatException("MD5 checksum check failed : \"" + md5sum + "\" <> \""
6620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    + downloadRecord.mAttributes.getAsString(MetadataDbHelper.CHECKSUM_COLUMN)
6630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    + "\"");
6640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
6650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
6660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
6680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Copies in to out using FileChannels.
6690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
6700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This tries to use channels for fast copying. If it doesn't work, fall back to
6710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * copyFileFallBack below.
6720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
6730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param in the stream to copy from.
6740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param out the stream to copy to.
6750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws IOException if both the normal and fallback methods raise exceptions.
6760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
6770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void copyFile(final InputStream in, final OutputStream out)
6780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            throws IOException {
67903118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Copying files");
6800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) {
68103118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa            DebugLogUtils.l("Not the right types");
6820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            copyFileFallback(in, out);
6830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } else {
6840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            try {
6850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final FileChannel sourceChannel = ((FileInputStream) in).getChannel();
6860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final FileChannel destinationChannel = ((FileOutputStream) out).getChannel();
6870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel);
6880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } catch (IOException e) {
6890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // Can't work with channels, or something went wrong. Copy by hand.
69003118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa                DebugLogUtils.l("Won't work");
6910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                copyFileFallback(in, out);
6920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
6930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
6940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
6950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
6960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
6970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Copies in to out with read/write methods, not FileChannels.
6980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
6990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param in the stream to copy from.
7000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param out the stream to copy to.
7010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws IOException if a read or a write fails.
7020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
7030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void copyFileFallback(final InputStream in, final OutputStream out)
7040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            throws IOException {
70503118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Falling back to slow copy");
7060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE];
7070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer))
7080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            out.write(buffer, 0, readBytes);
7090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
7100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
7110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
7120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Creates and returns a new file to store a dictionary
7130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to use to open the file.
7140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param locale the locale for this dictionary, to make the file name more readable.
7150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @return the file name, or throw an exception.
7160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @throws IOException if the file cannot be created.
7170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
7180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static String getTempFileName(final Context context, final String locale)
7190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            throws IOException {
72003118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Entering openTempFileOutput");
7210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final File dir = context.getFilesDir();
7220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final File f = File.createTempFile(locale + "___", DICT_FILE_SUFFIX, dir);
72303118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("File name is", f.getName());
7240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        return f.getName();
7250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
7260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
7270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
7280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Compare metadata (collections of word lists).
7290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
7300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This method takes whole metadata sets directly and compares them, matching the wordlists in
7310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * each of them on the id. It creates an ActionBatch object that can be .execute()'d to perform
7320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * the actual upgrade from `from' to `to'.
7330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
7340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to open databases on.
7350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
7360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param from the dictionary descriptor (as a list of wordlists) to upgrade from.
7370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param to the dictionary descriptor (as a list of wordlists) to upgrade to.
7380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @return an ordered list of runnables to be called to upgrade.
7390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
7400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static ActionBatch compareMetadataForUpgrade(final Context context,
7410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String clientId, List<WordListMetadata> from, List<WordListMetadata> to) {
7420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = new ActionBatch();
7430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Upgrade existing word lists
74403118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa        DebugLogUtils.l("Comparing dictionaries");
745a91561aa58db1c43092c1caecc051a11fa5391c7Tadashi G. Takaoka        final Set<String> wordListIds = new TreeSet<>();
7460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: Can these be null?
747a91561aa58db1c43092c1caecc051a11fa5391c7Tadashi G. Takaoka        if (null == from) from = new ArrayList<>();
748a91561aa58db1c43092c1caecc051a11fa5391c7Tadashi G. Takaoka        if (null == to) to = new ArrayList<>();
7490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (WordListMetadata wlData : from) wordListIds.add(wlData.mId);
7500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (WordListMetadata wlData : to) wordListIds.add(wlData.mId);
7510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        for (String id : wordListIds) {
7520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final WordListMetadata currentInfo = MetadataHandler.findWordListById(from, id);
7530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final WordListMetadata metadataInfo = MetadataHandler.findWordListById(to, id);
7540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // TODO: Remove the following unnecessary check, since we are now doing the filtering
7550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // inside findWordListById.
7560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final WordListMetadata newInfo = null == metadataInfo
7570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION
7580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            ? null : metadataInfo;
75903118a276014cd44d44d0d46f4f39622765e8e0cKen Wakasa            DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo);
7600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
7610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (null == currentInfo && null == newInfo) {
7620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // This may happen if a new word list appeared that we can't handle.
7630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (null == metadataInfo) {
7640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // What happened? Bug in Set<>?
7650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    Log.e(TAG, "Got an id for a wordlist that is neither in from nor in to");
7660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                } else {
7670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // We may come here if there is a new word list that we can't handle.
7680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format"
7690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            + " version " + metadataInfo.mFormatVersion + " and the maximum version"
770faa729c47c2d9ad739c288cc1ece910bc4e215e7Jean Chalard                            + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
7710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
7720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                continue;
7730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else if (null == currentInfo) {
7740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // This is the case where a new list that we did not know of popped on the server.
7750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // Make it available.
7760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo));
7770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else if (null == newInfo) {
7780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // This is the case where an old list we had is not in the server data any more.
7790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // Pass false to ForgetAction: this may be installed and we still want to apply
7800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // a forget-like action (remove the URL) if it is, so we want to turn off the
7810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // status == AVAILABLE check. If it's DELETING, this is the right thing to do,
7820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // as we want to leave the record as long as Android Keyboard has not deleted it ;
7830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                // the record will be removed when the file is actually deleted.
7840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, false));
7850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            } else {
7860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
7870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                if (newInfo.mVersion == currentInfo.mVersion) {
7880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // If it's the same id/version, we update the DB with the new values.
7890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // It doesn't matter too much if they didn't change.
7900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo));
7910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                } else if (newInfo.mVersion > currentInfo.mVersion) {
7920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // If it's a new version, it's a different entry in the database. Make it
7930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    // available, and if it's installed, also start the download.
7940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
7950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            currentInfo.mId, currentInfo.mVersion);
7960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
7970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo));
7980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    if (status == MetadataDbHelper.STATUS_INSTALLED
7990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            || status == MetadataDbHelper.STATUS_DISABLED) {
8000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo, false));
8010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    } else {
8020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        // Pass true to ForgetAction: this is indeed an update to a non-installed
8030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        // word list, so activate status == AVAILABLE check
8040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        // In case the status is DELETING, this is the right thing to do. It will
8050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        // leave the entry as DELETING and remove its URL so that Android Keyboard
8060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        // can delete it the next time it starts up.
8070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, true));
8080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    }
8090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                } else if (DEBUG) {
8100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    Log.i(TAG, "Not updating word list " + id
8110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                            + " : current list timestamp is " + currentInfo.mLastUpdate
8120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                                    + " ; new list timestamp is " + newInfo.mLastUpdate);
8130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                }
8140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
8150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
8160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        return actions;
8170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
8180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
8190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
8200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Computes an upgrade from the current state of the dictionaries to some desired state.
8210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context for reading settings and files.
8220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
8230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param newMetadata the state we want to upgrade to.
8240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @return the upgrade from the current state to the desired state, ready to be executed.
8250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
8260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static ActionBatch computeUpgradeTo(final Context context, final String clientId,
8270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final List<WordListMetadata> newMetadata) {
8280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final List<WordListMetadata> currentMetadata =
8290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.getCurrentMetadata(context, clientId);
8300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        return compareMetadataForUpgrade(context, clientId, currentMetadata, newMetadata);
8310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
8320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
8330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
8340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Shows the notification that informs the user a dictionary is available.
8350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
8360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * When this notification is clicked, the dialog for downloading the dictionary
8370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * over a metered connection is shown.
8380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
8390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    private static void showDictionaryAvailableNotification(final Context context,
8400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String clientId, final ContentValues installCandidate) {
8410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN);
8420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final Intent intent = new Intent();
8430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        intent.setClass(context, DownloadOverMeteredDialog.class);
8440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        intent.putExtra(DownloadOverMeteredDialog.CLIENT_ID_KEY, clientId);
8450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        intent.putExtra(DownloadOverMeteredDialog.WORDLIST_TO_DOWNLOAD_KEY,
8460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                installCandidate.getAsString(MetadataDbHelper.WORDLISTID_COLUMN));
8470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        intent.putExtra(DownloadOverMeteredDialog.SIZE_KEY,
8480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                installCandidate.getAsInteger(MetadataDbHelper.FILESIZE_COLUMN));
8490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        intent.putExtra(DownloadOverMeteredDialog.LOCALE_KEY, localeString);
8500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
8510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final PendingIntent notificationIntent = PendingIntent.getActivity(context,
8520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                0 /* requestCode */, intent,
8530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);
8540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final NotificationManager notificationManager =
8550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
8560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // None of those are expected to happen, but just in case...
8570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == notificationIntent || null == notificationManager) return;
8580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
8590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
8600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String language = (null == locale ? "" : locale.getDisplayLanguage());
8610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String titleFormat = context.getString(R.string.dict_available_notification_title);
8620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String notificationTitle = String.format(titleFormat, language);
863a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard        final Notification.Builder builder = new Notification.Builder(context)
8640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setAutoCancel(true)
8650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setContentIntent(notificationIntent)
8660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setContentTitle(notificationTitle)
8670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setContentText(context.getString(R.string.dict_available_notification_description))
8680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setTicker(notificationTitle)
8690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setOngoing(false)
8700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                .setOnlyAlertOnce(true)
871a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard                .setSmallIcon(R.drawable.ic_notify_dictionary);
872a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard        NotificationCompatUtils.setColor(builder,
873a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard                context.getResources().getColor(R.color.notification_accent_color));
874a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard        NotificationCompatUtils.setPriorityToLow(builder);
875a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard        NotificationCompatUtils.setVisibilityToSecret(builder);
876a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard        NotificationCompatUtils.setCategoryToRecommendation(builder);
877a2928484f6396cfc308acb7ac68c4a20455a118cJean Chalard        final Notification notification = NotificationCompatUtils.build(builder);
8780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        notificationManager.notify(DICT_AVAILABLE_NOTIFICATION_ID, notification);
8790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
8800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
8810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
8820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Installs a word list if it has never been requested.
8830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
8840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This is called when a word list is requested, and is available but not installed. It checks
8850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * the conditions for auto-installation: if the dictionary is a main dictionary for this
8860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * language, and it has never been opted out through the dictionary interface, then we start
8870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * installing it. For the user who enables a language and uses it for the first time, the
8880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * dictionary should magically start being used a short time after they start typing.
8890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * The mayPrompt argument indicates whether we should prompt the user for a decision to
8900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * download or not, in case we decide we are in the case where we should download - this
8910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * roughly happens when the current connectivity is 3G. See
8920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * DictionaryProvider#getDictionaryWordListsForContentUri for details.
8930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
8940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // As opposed to many other methods, this method does not need the version of the word
8950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // list because it may only install the latest version we know about for this specific
8960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // word list ID / client ID combination.
8970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void installIfNeverRequested(final Context context, final String clientId,
8980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String wordlistId, final boolean mayPrompt) {
8990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR);
9000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // If we have a new-format dictionary id (category:manual_id), then use the
9010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // specified category. Otherwise, it is a main dictionary, so force the
9020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // MAIN category upon it.
9030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String category = 2 == idArray.length ? idArray[0] : MAIN_DICTIONARY_CATEGORY;
9040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (!MAIN_DICTIONARY_CATEGORY.equals(category)) {
9050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // Not a main dictionary. We only auto-install main dictionaries, so we can return now.
9060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return;
9070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
9080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (CommonPreferences.getCommonPreferences(context).contains(wordlistId)) {
9090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // If some kind of settings has been done in the past for this specific id, then
9100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // this is not a candidate for auto-install. Because it already is either true,
9110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // in which case it may be installed or downloading or whatever, and we don't
9120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // need to care about it because it's already handled or being handled, or it's false
9130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // in which case it means the user explicitely turned it off and don't want to have
9140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // it installed. So we quit right away.
9150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return;
9160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
9170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
9180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
9190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ContentValues installCandidate =
9200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId);
9210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (MetadataDbHelper.STATUS_AVAILABLE
9220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                != installCandidate.getAsInteger(MetadataDbHelper.STATUS_COLUMN)) {
9230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // If it's not "AVAILABLE", we want to stop now. Because candidates for auto-install
9240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // are lists that we know are available, but we also know have never been installed.
9250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // It does obviously not concern already installed lists, or downloading lists,
9260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // or those that have been disabled, flagged as deleting... So anything else than
9270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            // AVAILABLE means we don't auto-install.
9280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            return;
9290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
9300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
9310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (mayPrompt
9320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                && DOWNLOAD_OVER_METERED_SETTING_UNKNOWN
9330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                        == getDownloadOverMeteredSetting(context)) {
9340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final ConnectivityManager cm =
9350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
9360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            if (ConnectivityManagerCompatUtils.isActiveNetworkMetered(cm)) {
9370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                showDictionaryAvailableNotification(context, clientId, installCandidate);
9380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                return;
9390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            }
9400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
9410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
9420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // We decided against prompting the user for a decision. This may be because we were
9430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // explicitly asked not to, or because we are currently on wi-fi anyway, or because we
9440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // already know the answer to the question. We'll enqueue a request ; StartDownloadAction
9450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // knows to use the correct type of network according to the current settings.
9460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
9470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // Also note that once it's auto-installed, a word list will be marked as INSTALLED. It will
9480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // thus receive automatic updates if there are any, which is what we want. If the user does
9490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // not want this word list, they will have to go to the settings and change them, which will
9500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // change the shared preferences. So there is no way for a word list that has been
9510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // auto-installed once to get auto-installed again, and that's what we want.
9520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = new ActionBatch();
9530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.add(new ActionBatch.StartDownloadAction(clientId,
9540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                WordListMetadata.createFromContentValues(installCandidate), false));
9550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN);
9560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // We are in a content provider: we can't do any UI at all. We have to defer the displaying
9570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // itself to the service. Also, we only display this when the user does not have a
9580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // dictionary for this language already: we know that from the mayPrompt argument.
9590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (mayPrompt) {
9600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final Intent intent = new Intent();
9610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            intent.setClass(context, DictionaryService.class);
9620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION);
9630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString);
9640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            context.startService(intent);
9650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
9660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.execute(context, new LogProblemReporter(TAG));
9670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
9680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
9690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
9700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Marks the word list with the passed id as used.
9710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
9720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This will download/install the list as required. The action will see that the destination
9730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * word list is a valid list, and take appropriate action - in this case, mark it as used.
9740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @see ActionBatch.Action#execute
9750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
9760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context for using action batches.
9770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
9780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param wordlistId the id of the word list to mark as installed.
9790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param version the version of the word list to mark as installed.
9800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param status the current status of the word list.
9810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param allowDownloadOnMeteredData whether to download even on metered data connection
9820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
9830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // The version argument is not used yet, because we don't need it to retrieve the information
9840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // we need. However, the pair (id, version) being the primary key to a word list in the database
9850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // it feels better for consistency to pass it, and some methods retrieving information about a
9860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // word list need it so we may need it in the future.
9870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void markAsUsed(final Context context, final String clientId,
9880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String wordlistId, final int version,
9890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final int status, final boolean allowDownloadOnMeteredData) {
9900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final List<WordListMetadata> currentMetadata =
9910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.getCurrentMetadata(context, clientId);
9920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        WordListMetadata wordList = MetadataHandler.findWordListById(currentMetadata, wordlistId);
9930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == wordList) return;
9940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = new ActionBatch();
9950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (MetadataDbHelper.STATUS_DISABLED == status
9960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                || MetadataDbHelper.STATUS_DELETING == status) {
9970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            actions.add(new ActionBatch.EnableAction(clientId, wordList));
9980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } else if (MetadataDbHelper.STATUS_AVAILABLE == status) {
9990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            actions.add(new ActionBatch.StartDownloadAction(clientId, wordList,
10000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                    allowDownloadOnMeteredData));
10010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        } else {
10020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status);
10030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        }
10040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.execute(context, new LogProblemReporter(TAG));
10050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        signalNewDictionaryState(context);
10060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
10070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
10080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
10090cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Marks the word list with the passed id as unused.
10100cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10110cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This leaves the file on the disk for ulterior use. The action will see that the destination
10120cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * word list is null, and take appropriate action - in this case, mark it as unused.
10130cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @see ActionBatch.Action#execute
10140cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10150cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context for using action batches.
10160cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
10170cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param wordlistId the id of the word list to mark as installed.
10180cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param version the version of the word list to mark as installed.
10190cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param status the current status of the word list.
10200cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
10210cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // The version and status arguments are not used yet, but this method matches its interface to
10220cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    // markAsUsed for consistency.
10230cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void markAsUnused(final Context context, final String clientId,
10240cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String wordlistId, final int version, final int status) {
10250cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final List<WordListMetadata> currentMetadata =
10260cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.getCurrentMetadata(context, clientId);
10270cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final WordListMetadata wordList =
10280cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.findWordListById(currentMetadata, wordlistId);
10290cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == wordList) return;
10300cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = new ActionBatch();
10310cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.add(new ActionBatch.DisableAction(clientId, wordList));
10320cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.execute(context, new LogProblemReporter(TAG));
10330cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        signalNewDictionaryState(context);
10340cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
10350cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
10360cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
10370cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Marks the word list with the passed id as deleting.
10380cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10390cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This basically means that on the next chance there is (right away if Android Keyboard
10400cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * happens to be up, or the next time it gets up otherwise) the dictionary pack will
10410cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * supply an empty dictionary to it that will replace whatever dictionary is installed.
10420cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This allows to release the space taken by a dictionary (except for the few bytes the
10430cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * empty dictionary takes up), and override a built-in default dictionary so that we
10440cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * can fake delete a built-in dictionary.
10450cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10460cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to open the database on.
10470cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
10480cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param wordlistId the id of the word list to mark as deleted.
10490cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param version the version of the word list to mark as deleted.
10500cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param status the current status of the word list.
10510cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
10520cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void markAsDeleting(final Context context, final String clientId,
10530cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String wordlistId, final int version, final int status) {
10540cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final List<WordListMetadata> currentMetadata =
10550cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.getCurrentMetadata(context, clientId);
10560cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final WordListMetadata wordList =
10570cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.findWordListById(currentMetadata, wordlistId);
10580cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == wordList) return;
10590cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = new ActionBatch();
10600cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.add(new ActionBatch.DisableAction(clientId, wordList));
10610cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.add(new ActionBatch.StartDeleteAction(clientId, wordList));
10620cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.execute(context, new LogProblemReporter(TAG));
10630cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        signalNewDictionaryState(context);
10640cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
10650cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
10660cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
10670cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Marks the word list with the passed id as actually deleted.
10680cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10690cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This reverts to available status or deletes the row as appropriate.
10700cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10710cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to open the database on.
10720cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
10730cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param wordlistId the id of the word list to mark as deleted.
10740cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param version the version of the word list to mark as deleted.
10750cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param status the current status of the word list.
10760cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
10770cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void markAsDeleted(final Context context, final String clientId,
10780cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String wordlistId, final int version, final int status) {
10790cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final List<WordListMetadata> currentMetadata =
10800cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.getCurrentMetadata(context, clientId);
10810cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final WordListMetadata wordList =
10820cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                MetadataHandler.findWordListById(currentMetadata, wordlistId);
10830cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        if (null == wordList) return;
10840cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        final ActionBatch actions = new ActionBatch();
10850cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.add(new ActionBatch.FinishDeleteAction(clientId, wordList));
10860cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        actions.execute(context, new LogProblemReporter(TAG));
10870cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        signalNewDictionaryState(context);
10880cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
10890cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard
10900cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    /**
10910cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * Marks the word list with the passed id as broken.
10920cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10930cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * This effectively deletes the entry from the metadata. It doesn't prevent the same
10940cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * word list to be downloaded again at a later time if the same or a new version is
10950cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * available the next time we download the metadata.
10960cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     *
10970cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param context the context to open the database on.
10980cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param clientId the id of the client.
10990cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param wordlistId the id of the word list to mark as broken.
11000cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     * @param version the version of the word list to mark as deleted.
11010cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard     */
11020cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    public static void markAsBroken(final Context context, final String clientId,
11030cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard            final String wordlistId, final int version) {
11040cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        // TODO: do this on another thread to avoid blocking the UI.
11050cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard        MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId),
11060cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard                wordlistId, version);
11070cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard    }
11080cc0544a2995c7eb54a830ae54db60af89d4073dJean Chalard}
1109