Home | History | Annotate | Download | only in dictionarypack
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.dictionarypack;
     18 
     19 import android.app.DownloadManager;
     20 import android.app.DownloadManager.Query;
     21 import android.app.DownloadManager.Request;
     22 import android.app.Notification;
     23 import android.app.NotificationManager;
     24 import android.app.PendingIntent;
     25 import android.content.ContentValues;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.SharedPreferences;
     29 import android.content.res.Resources;
     30 import android.database.Cursor;
     31 import android.database.sqlite.SQLiteDatabase;
     32 import android.net.ConnectivityManager;
     33 import android.net.Uri;
     34 import android.os.ParcelFileDescriptor;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 
     38 import com.android.inputmethod.compat.ConnectivityManagerCompatUtils;
     39 import com.android.inputmethod.compat.NotificationCompatUtils;
     40 import com.android.inputmethod.latin.R;
     41 import com.android.inputmethod.latin.common.LocaleUtils;
     42 import com.android.inputmethod.latin.makedict.FormatSpec;
     43 import com.android.inputmethod.latin.utils.ApplicationUtils;
     44 import com.android.inputmethod.latin.utils.DebugLogUtils;
     45 
     46 import java.io.File;
     47 import java.io.FileInputStream;
     48 import java.io.FileNotFoundException;
     49 import java.io.FileOutputStream;
     50 import java.io.IOException;
     51 import java.io.InputStream;
     52 import java.io.InputStreamReader;
     53 import java.io.OutputStream;
     54 import java.nio.channels.FileChannel;
     55 import java.util.ArrayList;
     56 import java.util.Collections;
     57 import java.util.LinkedList;
     58 import java.util.List;
     59 import java.util.Set;
     60 import java.util.TreeSet;
     61 
     62 import javax.annotation.Nullable;
     63 
     64 /**
     65  * Handler for the update process.
     66  *
     67  * This class is in charge of coordinating the update process for the various dictionaries
     68  * stored in the dictionary pack.
     69  */
     70 public final class UpdateHandler {
     71     static final String TAG = "DictionaryProvider:" + UpdateHandler.class.getSimpleName();
     72     private static final boolean DEBUG = DictionaryProvider.DEBUG;
     73 
     74     // Used to prevent trying to read the id of the downloaded file before it is written
     75     static final Object sSharedIdProtector = new Object();
     76 
     77     // Value used to mean this is not a real DownloadManager downloaded file id
     78     // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column
     79     // in SQLite, so it should never return anything < 0.
     80     public static final int NOT_AN_ID = -1;
     81     public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION =
     82             FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION;
     83 
     84     // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long.
     85     private static final int FILE_COPY_BUFFER_SIZE = 8192;
     86 
     87     // Table fixed values for metadata / downloads
     88     final static String METADATA_NAME = "metadata";
     89     final static int METADATA_TYPE = 0;
     90     final static int WORDLIST_TYPE = 1;
     91 
     92     // Suffix for generated dictionary files
     93     private static final String DICT_FILE_SUFFIX = ".dict";
     94     // Name of the category for the main dictionary
     95     public static final String MAIN_DICTIONARY_CATEGORY = "main";
     96 
     97     public static final String TEMP_DICT_FILE_SUB = "___";
     98 
     99     // The id for the "dictionary available" notification.
    100     static final int DICT_AVAILABLE_NOTIFICATION_ID = 1;
    101 
    102     /**
    103      * An interface for UIs or services that want to know when something happened.
    104      *
    105      * This is chiefly used by the dictionary manager UI.
    106      */
    107     public interface UpdateEventListener {
    108         void downloadedMetadata(boolean succeeded);
    109         void wordListDownloadFinished(String wordListId, boolean succeeded);
    110         void updateCycleCompleted();
    111     }
    112 
    113     /**
    114      * The list of currently registered listeners.
    115      */
    116     private static List<UpdateEventListener> sUpdateEventListeners
    117             = Collections.synchronizedList(new LinkedList<UpdateEventListener>());
    118 
    119     /**
    120      * Register a new listener to be notified of updates.
    121      *
    122      * Don't forget to call unregisterUpdateEventListener when done with it, or
    123      * it will leak the register.
    124      */
    125     public static void registerUpdateEventListener(final UpdateEventListener listener) {
    126         sUpdateEventListeners.add(listener);
    127     }
    128 
    129     /**
    130      * Unregister a previously registered listener.
    131      */
    132     public static void unregisterUpdateEventListener(final UpdateEventListener listener) {
    133         sUpdateEventListeners.remove(listener);
    134     }
    135 
    136     private static final String DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY = "downloadOverMetered";
    137 
    138     /**
    139      * Write the DownloadManager ID of the currently downloading metadata to permanent storage.
    140      *
    141      * @param context to open shared prefs
    142      * @param uri the uri of the metadata
    143      * @param downloadId the id returned by DownloadManager
    144      */
    145     private static void writeMetadataDownloadId(final Context context, final String uri,
    146             final long downloadId) {
    147         MetadataDbHelper.registerMetadataDownloadId(context, uri, downloadId);
    148     }
    149 
    150     public static final int DOWNLOAD_OVER_METERED_SETTING_UNKNOWN = 0;
    151     public static final int DOWNLOAD_OVER_METERED_ALLOWED = 1;
    152     public static final int DOWNLOAD_OVER_METERED_DISALLOWED = 2;
    153 
    154     /**
    155      * Sets the setting that tells us whether we may download over a metered connection.
    156      */
    157     public static void setDownloadOverMeteredSetting(final Context context,
    158             final boolean shouldDownloadOverMetered) {
    159         final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
    160         final SharedPreferences.Editor editor = prefs.edit();
    161         editor.putInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, shouldDownloadOverMetered
    162                 ? DOWNLOAD_OVER_METERED_ALLOWED : DOWNLOAD_OVER_METERED_DISALLOWED);
    163         editor.apply();
    164     }
    165 
    166     /**
    167      * Gets the setting that tells us whether we may download over a metered connection.
    168      *
    169      * This returns one of the constants above.
    170      */
    171     public static int getDownloadOverMeteredSetting(final Context context) {
    172         final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
    173         final int setting = prefs.getInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY,
    174                 DOWNLOAD_OVER_METERED_SETTING_UNKNOWN);
    175         return setting;
    176     }
    177 
    178     /**
    179      * Download latest metadata from the server through DownloadManager for all known clients
    180      * @param context The context for retrieving resources
    181      * @return true if an update successfully started, false otherwise.
    182      */
    183     public static boolean tryUpdate(final Context context) {
    184         // TODO: loop through all clients instead of only doing the default one.
    185         final TreeSet<String> uris = new TreeSet<>();
    186         final Cursor cursor = MetadataDbHelper.queryClientIds(context);
    187         if (null == cursor) return false;
    188         try {
    189             if (!cursor.moveToFirst()) return false;
    190             do {
    191                 final String clientId = cursor.getString(0);
    192                 final String metadataUri =
    193                         MetadataDbHelper.getMetadataUriAsString(context, clientId);
    194                 PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId));
    195                 DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
    196                 uris.add(metadataUri);
    197             } while (cursor.moveToNext());
    198         } finally {
    199             cursor.close();
    200         }
    201         boolean started = false;
    202         for (final String metadataUri : uris) {
    203             if (!TextUtils.isEmpty(metadataUri)) {
    204                 // If the metadata URI is empty, that means we should never update it at all.
    205                 // It should not be possible to come here with a null metadata URI, because
    206                 // it should have been rejected at the time of client registration; if there
    207                 // is a bug and it happens anyway, doing nothing is the right thing to do.
    208                 // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}.
    209                 updateClientsWithMetadataUri(context, metadataUri);
    210                 started = true;
    211             }
    212         }
    213         return started;
    214     }
    215 
    216     /**
    217      * Download latest metadata from the server through DownloadManager for all relevant clients
    218      *
    219      * @param context The context for retrieving resources
    220      * @param metadataUri The client to update
    221      */
    222     private static void updateClientsWithMetadataUri(
    223             final Context context, final String metadataUri) {
    224         Log.i(TAG, "updateClientsWithMetadataUri() : MetadataUri = " + metadataUri);
    225         // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
    226         // DownloadManager also stupidly cuts the extension to replace with its own that it
    227         // gets from the content-type. We need to circumvent this.
    228         final String disambiguator = "#" + System.currentTimeMillis()
    229                 + ApplicationUtils.getVersionName(context) + ".json";
    230         final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
    231         DebugLogUtils.l("Request =", metadataRequest);
    232 
    233         final Resources res = context.getResources();
    234         metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE);
    235         metadataRequest.setTitle(res.getString(R.string.download_description));
    236         // Do not show the notification when downloading the metadata.
    237         metadataRequest.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
    238         metadataRequest.setVisibleInDownloadsUi(
    239                 res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI));
    240 
    241         final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
    242         if (maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager,
    243                 DictionaryService.NO_CANCEL_DOWNLOAD_PERIOD_MILLIS)) {
    244             // We already have a recent download in progress. Don't register a new download.
    245             return;
    246         }
    247         final long downloadId;
    248         synchronized (sSharedIdProtector) {
    249             downloadId = manager.enqueue(metadataRequest);
    250             DebugLogUtils.l("Metadata download requested with id", downloadId);
    251             // If there is still a download in progress, it's been there for a while and
    252             // there is probably something wrong with download manager. It's best to just
    253             // overwrite the id and request it again. If the old one happens to finish
    254             // anyway, we don't know about its ID any more, so the downloadFinished
    255             // method will ignore it.
    256             writeMetadataDownloadId(context, metadataUri, downloadId);
    257         }
    258         Log.i(TAG, "updateClientsWithMetadataUri() : DownloadId = " + downloadId);
    259     }
    260 
    261     /**
    262      * Cancels downloading a file if there is one for this URI and it's too long.
    263      *
    264      * If we are not currently downloading the file at this URI, this is a no-op.
    265      *
    266      * @param context the context to open the database on
    267      * @param metadataUri the URI to cancel
    268      * @param manager an wrapped instance of DownloadManager
    269      * @param graceTime if there was a download started less than this many milliseconds, don't
    270      *  cancel and return true
    271      * @return whether the download is still active
    272      */
    273     private static boolean maybeCancelUpdateAndReturnIfStillRunning(final Context context,
    274             final String metadataUri, final DownloadManagerWrapper manager, final long graceTime) {
    275         synchronized (sSharedIdProtector) {
    276             final DownloadIdAndStartDate metadataDownloadIdAndStartDate =
    277                     MetadataDbHelper.getMetadataDownloadIdAndStartDateForURI(context, metadataUri);
    278             if (null == metadataDownloadIdAndStartDate) return false;
    279             if (NOT_AN_ID == metadataDownloadIdAndStartDate.mId) return false;
    280             if (metadataDownloadIdAndStartDate.mStartDate + graceTime
    281                     > System.currentTimeMillis()) {
    282                 return true;
    283             }
    284             manager.remove(metadataDownloadIdAndStartDate.mId);
    285             writeMetadataDownloadId(context, metadataUri, NOT_AN_ID);
    286         }
    287         // Consider a cancellation as a failure. As such, inform listeners that the download
    288         // has failed.
    289         for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
    290             listener.downloadedMetadata(false);
    291         }
    292         return false;
    293     }
    294 
    295     /**
    296      * Cancels a pending update for this client, if there is one.
    297      *
    298      * If we are not currently updating metadata for this client, this is a no-op. This is a helper
    299      * method that gets the download manager service and the metadata URI for this client.
    300      *
    301      * @param context the context, to get an instance of DownloadManager
    302      * @param clientId the ID of the client we want to cancel the update of
    303      */
    304     public static void cancelUpdate(final Context context, final String clientId) {
    305         final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
    306         final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId);
    307         maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, 0 /* graceTime */);
    308     }
    309 
    310     /**
    311      * Registers a download request and flags it as downloading in the metadata table.
    312      *
    313      * This is a helper method that exists to avoid race conditions where DownloadManager might
    314      * finish downloading the file before the data is committed to the database.
    315      * It registers the request with the DownloadManager service and also updates the metadata
    316      * database directly within a synchronized section.
    317      * This method has no intelligence about the data it commits to the database aside from the
    318      * download request id, which is not known before submitting the request to the download
    319      * manager. Hence, it only updates the relevant line.
    320      *
    321      * @param manager a wrapped download manager service to register the request with.
    322      * @param request the request to register.
    323      * @param db the metadata database.
    324      * @param id the id of the word list.
    325      * @param version the version of the word list.
    326      * @return the download id returned by the download manager.
    327      */
    328     public static long registerDownloadRequest(final DownloadManagerWrapper manager,
    329             final Request request, final SQLiteDatabase db, final String id, final int version) {
    330         Log.i(TAG, "registerDownloadRequest() : Id = " + id + " : Version = " + version);
    331         final long downloadId;
    332         synchronized (sSharedIdProtector) {
    333             downloadId = manager.enqueue(request);
    334             Log.i(TAG, "registerDownloadRequest() : DownloadId = " + downloadId);
    335             MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId);
    336         }
    337         return downloadId;
    338     }
    339 
    340     /**
    341      * Retrieve information about a specific download from DownloadManager.
    342      */
    343     private static CompletedDownloadInfo getCompletedDownloadInfo(
    344             final DownloadManagerWrapper manager, final long downloadId) {
    345         final Query query = new Query().setFilterById(downloadId);
    346         final Cursor cursor = manager.query(query);
    347 
    348         if (null == cursor) {
    349             return new CompletedDownloadInfo(null, downloadId, DownloadManager.STATUS_FAILED);
    350         }
    351         try {
    352             final String uri;
    353             final int status;
    354             if (cursor.moveToNext()) {
    355                 final int columnStatus = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
    356                 final int columnError = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
    357                 final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI);
    358                 final int error = cursor.getInt(columnError);
    359                 status = cursor.getInt(columnStatus);
    360                 final String uriWithAnchor = cursor.getString(columnUri);
    361                 int anchorIndex = uriWithAnchor.indexOf('#');
    362                 if (anchorIndex != -1) {
    363                     uri = uriWithAnchor.substring(0, anchorIndex);
    364                 } else {
    365                     uri = uriWithAnchor;
    366                 }
    367                 if (DownloadManager.STATUS_SUCCESSFUL != status) {
    368                     Log.e(TAG, "Permanent failure of download " + downloadId
    369                             + " with error code: " + error);
    370                 }
    371             } else {
    372                 uri = null;
    373                 status = DownloadManager.STATUS_FAILED;
    374             }
    375             return new CompletedDownloadInfo(uri, downloadId, status);
    376         } finally {
    377             cursor.close();
    378         }
    379     }
    380 
    381     private static ArrayList<DownloadRecord> getDownloadRecordsForCompletedDownloadInfo(
    382             final Context context, final CompletedDownloadInfo downloadInfo) {
    383         // Get and check the ID of the file we are waiting for, compare them to downloaded ones
    384         synchronized(sSharedIdProtector) {
    385             final ArrayList<DownloadRecord> downloadRecords =
    386                     MetadataDbHelper.getDownloadRecordsForDownloadId(context,
    387                             downloadInfo.mDownloadId);
    388             // If any of these is metadata, we should update the DB
    389             boolean hasMetadata = false;
    390             for (DownloadRecord record : downloadRecords) {
    391                 if (record.isMetadata()) {
    392                     hasMetadata = true;
    393                     break;
    394                 }
    395             }
    396             if (hasMetadata) {
    397                 writeMetadataDownloadId(context, downloadInfo.mUri, NOT_AN_ID);
    398                 MetadataDbHelper.saveLastUpdateTimeOfUri(context, downloadInfo.mUri);
    399             }
    400             return downloadRecords;
    401         }
    402     }
    403 
    404     /**
    405      * Take appropriate action after a download finished, in success or in error.
    406      *
    407      * This is called by the system upon broadcast from the DownloadManager that a file
    408      * has been downloaded successfully.
    409      * After a simple check that this is actually the file we are waiting for, this
    410      * method basically coordinates the parsing and comparison of metadata, and fires
    411      * the computation of the list of actions that should be taken then executes them.
    412      *
    413      * @param context The context for this action.
    414      * @param intent The intent from the DownloadManager containing details about the download.
    415      */
    416     /* package */ static void downloadFinished(final Context context, final Intent intent) {
    417         // Get and check the ID of the file that was downloaded
    418         final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID);
    419         Log.i(TAG, "downloadFinished() : DownloadId = " + fileId);
    420         if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
    421 
    422         final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
    423         final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId);
    424 
    425         final ArrayList<DownloadRecord> recordList =
    426                 getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo);
    427         if (null == recordList) return; // It was someone else's download.
    428         DebugLogUtils.l("Received result for download ", fileId);
    429 
    430         // TODO: handle gracefully a null pointer here. This is practically impossible because
    431         // we come here only when DownloadManager explicitly called us when it ended a
    432         // download, so we are pretty sure it's alive. It's theoretically possible that it's
    433         // disabled right inbetween the firing of the intent and the control reaching here.
    434 
    435         for (final DownloadRecord record : recordList) {
    436             // downloadSuccessful is not final because we may still have exceptions from now on
    437             boolean downloadSuccessful = false;
    438             try {
    439                 if (downloadInfo.wasSuccessful()) {
    440                     downloadSuccessful = handleDownloadedFile(context, record, manager, fileId);
    441                     Log.i(TAG, "downloadFinished() : Success = " + downloadSuccessful);
    442                 }
    443             } finally {
    444                 final String resultMessage = downloadSuccessful ? "Success" : "Failure";
    445                 if (record.isMetadata()) {
    446                     Log.i(TAG, "downloadFinished() : Metadata " + resultMessage);
    447                     publishUpdateMetadataCompleted(context, downloadSuccessful);
    448                 } else {
    449                     Log.i(TAG, "downloadFinished() : WordList " + resultMessage);
    450                     final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId);
    451                     publishUpdateWordListCompleted(context, downloadSuccessful, fileId,
    452                             db, record.mAttributes, record.mClientId);
    453                 }
    454             }
    455         }
    456         // Now that we're done using it, we can remove this download from DLManager
    457         manager.remove(fileId);
    458     }
    459 
    460     /**
    461      * Sends a broadcast informing listeners that the dictionaries were updated.
    462      *
    463      * This will call all local listeners through the UpdateEventListener#downloadedMetadata
    464      * callback (for example, the dictionary provider interface uses this to stop the Loading
    465      * animation) and send a broadcast about the metadata having been updated. For a client of
    466      * the dictionary pack like Latin IME, this means it should re-query the dictionary pack
    467      * for any relevant new data.
    468      *
    469      * @param context the context, to send the broadcast.
    470      * @param downloadSuccessful whether the download of the metadata was successful or not.
    471      */
    472     public static void publishUpdateMetadataCompleted(final Context context,
    473             final boolean downloadSuccessful) {
    474         // We need to warn all listeners of what happened. But some listeners may want to
    475         // remove themselves or re-register something in response. Hence we should take a
    476         // snapshot of the listener list and warn them all. This also prevents any
    477         // concurrent modification problem of the static list.
    478         for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
    479             listener.downloadedMetadata(downloadSuccessful);
    480         }
    481         publishUpdateCycleCompletedEvent(context);
    482     }
    483 
    484     private static void publishUpdateWordListCompleted(final Context context,
    485             final boolean downloadSuccessful, final long fileId,
    486             final SQLiteDatabase db, final ContentValues downloadedFileRecord,
    487             final String clientId) {
    488         synchronized(sSharedIdProtector) {
    489             if (downloadSuccessful) {
    490                 final ActionBatch actions = new ActionBatch();
    491                 actions.add(new ActionBatch.InstallAfterDownloadAction(clientId,
    492                         downloadedFileRecord));
    493                 actions.execute(context, new LogProblemReporter(TAG));
    494             } else {
    495                 MetadataDbHelper.deleteDownloadingEntry(db, fileId);
    496             }
    497         }
    498         // See comment above about #linkedCopyOfLists
    499         for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
    500             listener.wordListDownloadFinished(downloadedFileRecord.getAsString(
    501                             MetadataDbHelper.WORDLISTID_COLUMN), downloadSuccessful);
    502         }
    503         publishUpdateCycleCompletedEvent(context);
    504     }
    505 
    506     private static void publishUpdateCycleCompletedEvent(final Context context) {
    507         // Even if this is not successful, we have to publish the new state.
    508         PrivateLog.log("Publishing update cycle completed event");
    509         DebugLogUtils.l("Publishing update cycle completed event");
    510         for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
    511             listener.updateCycleCompleted();
    512         }
    513         signalNewDictionaryState(context);
    514     }
    515 
    516     private static boolean handleDownloadedFile(final Context context,
    517             final DownloadRecord downloadRecord, final DownloadManagerWrapper manager,
    518             final long fileId) {
    519         try {
    520             // {@link handleWordList(Context,InputStream,ContentValues)}.
    521             // Handle the downloaded file according to its type
    522             if (downloadRecord.isMetadata()) {
    523                 DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
    524                 // #handleMetadata() closes its InputStream argument
    525                 handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream(
    526                         manager.openDownloadedFile(fileId)), downloadRecord.mClientId);
    527             } else {
    528                 DebugLogUtils.l("Data D/L'd is a word list");
    529                 final int wordListStatus = downloadRecord.mAttributes.getAsInteger(
    530                         MetadataDbHelper.STATUS_COLUMN);
    531                 if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) {
    532                     // #handleWordList() closes its InputStream argument
    533                     handleWordList(context, new ParcelFileDescriptor.AutoCloseInputStream(
    534                             manager.openDownloadedFile(fileId)), downloadRecord);
    535                 } else {
    536                     Log.e(TAG, "Spurious download ended. Maybe a cancelled download?");
    537                 }
    538             }
    539             return true;
    540         } catch (FileNotFoundException e) {
    541             Log.e(TAG, "A file was downloaded but it can't be opened", e);
    542         } catch (IOException e) {
    543             // Can't read the file... disk damage?
    544             Log.e(TAG, "Can't read a file", e);
    545             // TODO: Check with UX how we should warn the user.
    546         } catch (IllegalStateException e) {
    547             // The format of the downloaded file is incorrect. We should maybe report upstream?
    548             Log.e(TAG, "Incorrect data received", e);
    549         } catch (BadFormatException e) {
    550             // The format of the downloaded file is incorrect. We should maybe report upstream?
    551             Log.e(TAG, "Incorrect data received", e);
    552         }
    553         return false;
    554     }
    555 
    556     /**
    557      * Returns a copy of the specified list, with all elements copied.
    558      *
    559      * This returns a linked list.
    560      */
    561     private static <T> List<T> linkedCopyOfList(final List<T> src) {
    562         // Instantiation of a parameterized type is not possible in Java, so it's not possible to
    563         // return the same type of list that was passed - probably the same reason why Collections
    564         // does not do it. So we need to decide statically which concrete type to return.
    565         return new LinkedList<>(src);
    566     }
    567 
    568     /**
    569      * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data.
    570      */
    571     private static void signalNewDictionaryState(final Context context) {
    572         // TODO: Also provide the locale of the updated dictionary so that the LatinIme
    573         // does not have to reset if it is a different locale.
    574         final Intent newDictBroadcast =
    575                 new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
    576         context.sendBroadcast(newDictBroadcast);
    577     }
    578 
    579     /**
    580      * Parse metadata and take appropriate action (that is, upgrade dictionaries).
    581      * @param context the context to read settings.
    582      * @param stream an input stream pointing to the downloaded data. May not be null.
    583      *  Will be closed upon finishing.
    584      * @param clientId the ID of the client to update
    585      * @throws BadFormatException if the metadata is not in a known format.
    586      * @throws IOException if the downloaded file can't be read from the disk
    587      */
    588     public static void handleMetadata(final Context context, final InputStream stream,
    589             final String clientId) throws IOException, BadFormatException {
    590         DebugLogUtils.l("Entering handleMetadata");
    591         final List<WordListMetadata> newMetadata;
    592         final InputStreamReader reader = new InputStreamReader(stream);
    593         try {
    594             // According to the doc InputStreamReader buffers, so no need to add a buffering layer
    595             newMetadata = MetadataHandler.readMetadata(reader);
    596         } finally {
    597             reader.close();
    598         }
    599 
    600         DebugLogUtils.l("Downloaded metadata :", newMetadata);
    601         PrivateLog.log("Downloaded metadata\n" + newMetadata);
    602 
    603         final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata);
    604         // TODO: Check with UX how we should report to the user
    605         // TODO: add an action to close the database
    606         actions.execute(context, new LogProblemReporter(TAG));
    607     }
    608 
    609     /**
    610      * Handle a word list: put it in its right place, and update the passed content values.
    611      * @param context the context for opening files.
    612      * @param inputStream an input stream pointing to the downloaded data. May not be null.
    613      *  Will be closed upon finishing.
    614      * @param downloadRecord the content values to fill the file name in.
    615      * @throws IOException if files can't be read or written.
    616      * @throws BadFormatException if the md5 checksum doesn't match the metadata.
    617      */
    618     private static void handleWordList(final Context context,
    619             final InputStream inputStream, final DownloadRecord downloadRecord)
    620             throws IOException, BadFormatException {
    621 
    622         // DownloadManager does not have the ability to put the file directly where we want
    623         // it, so we had it download to a temporary place. Now we move it. It will be deleted
    624         // automatically by DownloadManager.
    625         DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
    626                 MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
    627         PrivateLog.log("Downloaded a new word list with description : "
    628                 + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
    629                 + " for " + downloadRecord.mClientId);
    630 
    631         final String locale =
    632                 downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN);
    633         final String destinationFile = getTempFileName(context, locale);
    634         downloadRecord.mAttributes.put(MetadataDbHelper.LOCAL_FILENAME_COLUMN, destinationFile);
    635 
    636         FileOutputStream outputStream = null;
    637         try {
    638             outputStream = context.openFileOutput(destinationFile, Context.MODE_PRIVATE);
    639             copyFile(inputStream, outputStream);
    640         } finally {
    641             inputStream.close();
    642             if (outputStream != null) {
    643                 outputStream.close();
    644             }
    645         }
    646 
    647         // TODO: Consolidate this MD5 calculation with file copying above.
    648         // We need to reopen the file because the inputstream bytes have been consumed, and there
    649         // is nothing in InputStream to reopen or rewind the stream
    650         FileInputStream copiedFile = null;
    651         final String md5sum;
    652         try {
    653             copiedFile = context.openFileInput(destinationFile);
    654             md5sum = MD5Calculator.checksum(copiedFile);
    655         } finally {
    656             if (copiedFile != null) {
    657                 copiedFile.close();
    658             }
    659         }
    660         if (TextUtils.isEmpty(md5sum)) {
    661             return; // We can't compute the checksum anyway, so return and hope for the best
    662         }
    663         if (!md5sum.equals(downloadRecord.mAttributes.getAsString(
    664                 MetadataDbHelper.CHECKSUM_COLUMN))) {
    665             context.deleteFile(destinationFile);
    666             throw new BadFormatException("MD5 checksum check failed : \"" + md5sum + "\" <> \""
    667                     + downloadRecord.mAttributes.getAsString(MetadataDbHelper.CHECKSUM_COLUMN)
    668                     + "\"");
    669         }
    670     }
    671 
    672     /**
    673      * Copies in to out using FileChannels.
    674      *
    675      * This tries to use channels for fast copying. If it doesn't work, fall back to
    676      * copyFileFallBack below.
    677      *
    678      * @param in the stream to copy from.
    679      * @param out the stream to copy to.
    680      * @throws IOException if both the normal and fallback methods raise exceptions.
    681      */
    682     private static void copyFile(final InputStream in, final OutputStream out)
    683             throws IOException {
    684         DebugLogUtils.l("Copying files");
    685         if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) {
    686             DebugLogUtils.l("Not the right types");
    687             copyFileFallback(in, out);
    688         } else {
    689             try {
    690                 final FileChannel sourceChannel = ((FileInputStream) in).getChannel();
    691                 final FileChannel destinationChannel = ((FileOutputStream) out).getChannel();
    692                 sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel);
    693             } catch (IOException e) {
    694                 // Can't work with channels, or something went wrong. Copy by hand.
    695                 DebugLogUtils.l("Won't work");
    696                 copyFileFallback(in, out);
    697             }
    698         }
    699     }
    700 
    701     /**
    702      * Copies in to out with read/write methods, not FileChannels.
    703      *
    704      * @param in the stream to copy from.
    705      * @param out the stream to copy to.
    706      * @throws IOException if a read or a write fails.
    707      */
    708     private static void copyFileFallback(final InputStream in, final OutputStream out)
    709             throws IOException {
    710         DebugLogUtils.l("Falling back to slow copy");
    711         final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE];
    712         for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer))
    713             out.write(buffer, 0, readBytes);
    714     }
    715 
    716     /**
    717      * Creates and returns a new file to store a dictionary
    718      * @param context the context to use to open the file.
    719      * @param locale the locale for this dictionary, to make the file name more readable.
    720      * @return the file name, or throw an exception.
    721      * @throws IOException if the file cannot be created.
    722      */
    723     private static String getTempFileName(final Context context, final String locale)
    724             throws IOException {
    725         DebugLogUtils.l("Entering openTempFileOutput");
    726         final File dir = context.getFilesDir();
    727         final File f = File.createTempFile(locale + TEMP_DICT_FILE_SUB, DICT_FILE_SUFFIX, dir);
    728         DebugLogUtils.l("File name is", f.getName());
    729         return f.getName();
    730     }
    731 
    732     /**
    733      * Compare metadata (collections of word lists).
    734      *
    735      * This method takes whole metadata sets directly and compares them, matching the wordlists in
    736      * each of them on the id. It creates an ActionBatch object that can be .execute()'d to perform
    737      * the actual upgrade from `from' to `to'.
    738      *
    739      * @param context the context to open databases on.
    740      * @param clientId the id of the client.
    741      * @param from the dictionary descriptor (as a list of wordlists) to upgrade from.
    742      * @param to the dictionary descriptor (as a list of wordlists) to upgrade to.
    743      * @return an ordered list of runnables to be called to upgrade.
    744      */
    745     private static ActionBatch compareMetadataForUpgrade(final Context context,
    746             final String clientId, @Nullable final List<WordListMetadata> from,
    747             @Nullable final List<WordListMetadata> to) {
    748         final ActionBatch actions = new ActionBatch();
    749         // Upgrade existing word lists
    750         DebugLogUtils.l("Comparing dictionaries");
    751         final Set<String> wordListIds = new TreeSet<>();
    752         // TODO: Can these be null?
    753         final List<WordListMetadata> fromList = (from == null) ? new ArrayList<WordListMetadata>()
    754                 : from;
    755         final List<WordListMetadata> toList = (to == null) ? new ArrayList<WordListMetadata>()
    756                 : to;
    757         for (WordListMetadata wlData : fromList) wordListIds.add(wlData.mId);
    758         for (WordListMetadata wlData : toList) wordListIds.add(wlData.mId);
    759         for (String id : wordListIds) {
    760             final WordListMetadata currentInfo = MetadataHandler.findWordListById(fromList, id);
    761             final WordListMetadata metadataInfo = MetadataHandler.findWordListById(toList, id);
    762             // TODO: Remove the following unnecessary check, since we are now doing the filtering
    763             // inside findWordListById.
    764             final WordListMetadata newInfo = null == metadataInfo
    765                     || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION
    766                             ? null : metadataInfo;
    767             DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo);
    768 
    769             if (null == currentInfo && null == newInfo) {
    770                 // This may happen if a new word list appeared that we can't handle.
    771                 if (null == metadataInfo) {
    772                     // What happened? Bug in Set<>?
    773                     Log.e(TAG, "Got an id for a wordlist that is neither in from nor in to");
    774                 } else {
    775                     // We may come here if there is a new word list that we can't handle.
    776                     Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format"
    777                             + " version " + metadataInfo.mFormatVersion + " and the maximum version"
    778                             + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
    779                 }
    780                 continue;
    781             } else if (null == currentInfo) {
    782                 // This is the case where a new list that we did not know of popped on the server.
    783                 // Make it available.
    784                 actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo));
    785             } else if (null == newInfo) {
    786                 // This is the case where an old list we had is not in the server data any more.
    787                 // Pass false to ForgetAction: this may be installed and we still want to apply
    788                 // a forget-like action (remove the URL) if it is, so we want to turn off the
    789                 // status == AVAILABLE check. If it's DELETING, this is the right thing to do,
    790                 // as we want to leave the record as long as Android Keyboard has not deleted it ;
    791                 // the record will be removed when the file is actually deleted.
    792                 actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, false));
    793             } else {
    794                 final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
    795                 if (newInfo.mVersion == currentInfo.mVersion) {
    796                     if (TextUtils.equals(newInfo.mRemoteFilename, currentInfo.mRemoteFilename)) {
    797                         // If the dictionary url hasn't changed, we should preserve the retryCount.
    798                         newInfo.mRetryCount = currentInfo.mRetryCount;
    799                     }
    800                     // If it's the same id/version, we update the DB with the new values.
    801                     // It doesn't matter too much if they didn't change.
    802                     actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo));
    803                 } else if (newInfo.mVersion > currentInfo.mVersion) {
    804                     // If it's a new version, it's a different entry in the database. Make it
    805                     // available, and if it's installed, also start the download.
    806                     final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
    807                             currentInfo.mId, currentInfo.mVersion);
    808                     final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
    809                     actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo));
    810                     if (status == MetadataDbHelper.STATUS_INSTALLED
    811                             || status == MetadataDbHelper.STATUS_DISABLED) {
    812                         actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo));
    813                     } else {
    814                         // Pass true to ForgetAction: this is indeed an update to a non-installed
    815                         // word list, so activate status == AVAILABLE check
    816                         // In case the status is DELETING, this is the right thing to do. It will
    817                         // leave the entry as DELETING and remove its URL so that Android Keyboard
    818                         // can delete it the next time it starts up.
    819                         actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, true));
    820                     }
    821                 } else if (DEBUG) {
    822                     Log.i(TAG, "Not updating word list " + id
    823                             + " : current list timestamp is " + currentInfo.mLastUpdate
    824                                     + " ; new list timestamp is " + newInfo.mLastUpdate);
    825                 }
    826             }
    827         }
    828         return actions;
    829     }
    830 
    831     /**
    832      * Computes an upgrade from the current state of the dictionaries to some desired state.
    833      * @param context the context for reading settings and files.
    834      * @param clientId the id of the client.
    835      * @param newMetadata the state we want to upgrade to.
    836      * @return the upgrade from the current state to the desired state, ready to be executed.
    837      */
    838     public static ActionBatch computeUpgradeTo(final Context context, final String clientId,
    839             final List<WordListMetadata> newMetadata) {
    840         final List<WordListMetadata> currentMetadata =
    841                 MetadataHandler.getCurrentMetadata(context, clientId);
    842         return compareMetadataForUpgrade(context, clientId, currentMetadata, newMetadata);
    843     }
    844 
    845     /**
    846      * Shows the notification that informs the user a dictionary is available.
    847      *
    848      * When this notification is clicked, the dialog for downloading the dictionary
    849      * over a metered connection is shown.
    850      */
    851     private static void showDictionaryAvailableNotification(final Context context,
    852             final String clientId, final ContentValues installCandidate) {
    853         final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN);
    854         final Intent intent = new Intent();
    855         intent.setClass(context, DownloadOverMeteredDialog.class);
    856         intent.putExtra(DownloadOverMeteredDialog.CLIENT_ID_KEY, clientId);
    857         intent.putExtra(DownloadOverMeteredDialog.WORDLIST_TO_DOWNLOAD_KEY,
    858                 installCandidate.getAsString(MetadataDbHelper.WORDLISTID_COLUMN));
    859         intent.putExtra(DownloadOverMeteredDialog.SIZE_KEY,
    860                 installCandidate.getAsInteger(MetadataDbHelper.FILESIZE_COLUMN));
    861         intent.putExtra(DownloadOverMeteredDialog.LOCALE_KEY, localeString);
    862         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    863         final PendingIntent notificationIntent = PendingIntent.getActivity(context,
    864                 0 /* requestCode */, intent,
    865                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);
    866         final NotificationManager notificationManager =
    867                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    868         // None of those are expected to happen, but just in case...
    869         if (null == notificationIntent || null == notificationManager) return;
    870 
    871         final String language = (null == localeString) ? ""
    872                 : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage();
    873         final String titleFormat = context.getString(R.string.dict_available_notification_title);
    874         final String notificationTitle = String.format(titleFormat, language);
    875         final Notification.Builder builder = new Notification.Builder(context)
    876                 .setAutoCancel(true)
    877                 .setContentIntent(notificationIntent)
    878                 .setContentTitle(notificationTitle)
    879                 .setContentText(context.getString(R.string.dict_available_notification_description))
    880                 .setTicker(notificationTitle)
    881                 .setOngoing(false)
    882                 .setOnlyAlertOnce(true)
    883                 .setSmallIcon(R.drawable.ic_notify_dictionary);
    884         NotificationCompatUtils.setColor(builder,
    885                 context.getResources().getColor(R.color.notification_accent_color));
    886         NotificationCompatUtils.setPriorityToLow(builder);
    887         NotificationCompatUtils.setVisibilityToSecret(builder);
    888         NotificationCompatUtils.setCategoryToRecommendation(builder);
    889         final Notification notification = NotificationCompatUtils.build(builder);
    890         notificationManager.notify(DICT_AVAILABLE_NOTIFICATION_ID, notification);
    891     }
    892 
    893     /**
    894      * Installs a word list if it has never been requested.
    895      *
    896      * This is called when a word list is requested, and is available but not installed. It checks
    897      * the conditions for auto-installation: if the dictionary is a main dictionary for this
    898      * language, and it has never been opted out through the dictionary interface, then we start
    899      * installing it. For the user who enables a language and uses it for the first time, the
    900      * dictionary should magically start being used a short time after they start typing.
    901      * The mayPrompt argument indicates whether we should prompt the user for a decision to
    902      * download or not, in case we decide we are in the case where we should download - this
    903      * roughly happens when the current connectivity is 3G. See
    904      * DictionaryProvider#getDictionaryWordListsForContentUri for details.
    905      */
    906     // As opposed to many other methods, this method does not need the version of the word
    907     // list because it may only install the latest version we know about for this specific
    908     // word list ID / client ID combination.
    909     public static void installIfNeverRequested(final Context context, final String clientId,
    910             final String wordlistId) {
    911         Log.i(TAG, "installIfNeverRequested() : ClientId = " + clientId
    912                 + " : WordListId = " + wordlistId);
    913         final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR);
    914         // If we have a new-format dictionary id (category:manual_id), then use the
    915         // specified category. Otherwise, it is a main dictionary, so force the
    916         // MAIN category upon it.
    917         final String category = 2 == idArray.length ? idArray[0] : MAIN_DICTIONARY_CATEGORY;
    918         if (!MAIN_DICTIONARY_CATEGORY.equals(category)) {
    919             // Not a main dictionary. We only auto-install main dictionaries, so we can return now.
    920             return;
    921         }
    922         if (CommonPreferences.getCommonPreferences(context).contains(wordlistId)) {
    923             // If some kind of settings has been done in the past for this specific id, then
    924             // this is not a candidate for auto-install. Because it already is either true,
    925             // in which case it may be installed or downloading or whatever, and we don't
    926             // need to care about it because it's already handled or being handled, or it's false
    927             // in which case it means the user explicitely turned it off and don't want to have
    928             // it installed. So we quit right away.
    929             return;
    930         }
    931 
    932         final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
    933         final ContentValues installCandidate =
    934                 MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId);
    935         if (MetadataDbHelper.STATUS_AVAILABLE
    936                 != installCandidate.getAsInteger(MetadataDbHelper.STATUS_COLUMN)) {
    937             // If it's not "AVAILABLE", we want to stop now. Because candidates for auto-install
    938             // are lists that we know are available, but we also know have never been installed.
    939             // It does obviously not concern already installed lists, or downloading lists,
    940             // or those that have been disabled, flagged as deleting... So anything else than
    941             // AVAILABLE means we don't auto-install.
    942             return;
    943         }
    944 
    945         // We decided against prompting the user for a decision. This may be because we were
    946         // explicitly asked not to, or because we are currently on wi-fi anyway, or because we
    947         // already know the answer to the question. We'll enqueue a request ; StartDownloadAction
    948         // knows to use the correct type of network according to the current settings.
    949 
    950         // Also note that once it's auto-installed, a word list will be marked as INSTALLED. It will
    951         // thus receive automatic updates if there are any, which is what we want. If the user does
    952         // not want this word list, they will have to go to the settings and change them, which will
    953         // change the shared preferences. So there is no way for a word list that has been
    954         // auto-installed once to get auto-installed again, and that's what we want.
    955         final ActionBatch actions = new ActionBatch();
    956         WordListMetadata metadata = WordListMetadata.createFromContentValues(installCandidate);
    957         actions.add(new ActionBatch.StartDownloadAction(clientId, metadata));
    958         final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN);
    959         // We are in a content provider: we can't do any UI at all. We have to defer the displaying
    960         // itself to the service. Also, we only display this when the user does not have a
    961         // dictionary for this language already.
    962         final Intent intent = new Intent();
    963         intent.setClass(context, DictionaryService.class);
    964         intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION);
    965         intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString);
    966         context.startService(intent);
    967         Log.i(TAG, "installIfNeverRequested() : StartDownloadAction for " + metadata);
    968         actions.execute(context, new LogProblemReporter(TAG));
    969     }
    970 
    971     /**
    972      * Marks the word list with the passed id as used.
    973      *
    974      * This will download/install the list as required. The action will see that the destination
    975      * word list is a valid list, and take appropriate action - in this case, mark it as used.
    976      * @see ActionBatch.Action#execute
    977      *
    978      * @param context the context for using action batches.
    979      * @param clientId the id of the client.
    980      * @param wordlistId the id of the word list to mark as installed.
    981      * @param version the version of the word list to mark as installed.
    982      * @param status the current status of the word list.
    983      * @param allowDownloadOnMeteredData whether to download even on metered data connection
    984      */
    985     // The version argument is not used yet, because we don't need it to retrieve the information
    986     // we need. However, the pair (id, version) being the primary key to a word list in the database
    987     // it feels better for consistency to pass it, and some methods retrieving information about a
    988     // word list need it so we may need it in the future.
    989     public static void markAsUsed(final Context context, final String clientId,
    990             final String wordlistId, final int version,
    991             final int status, final boolean allowDownloadOnMeteredData) {
    992         final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList(
    993                 context, clientId, wordlistId, version);
    994 
    995         if (null == wordListMetaData) return;
    996 
    997         final ActionBatch actions = new ActionBatch();
    998         if (MetadataDbHelper.STATUS_DISABLED == status
    999                 || MetadataDbHelper.STATUS_DELETING == status) {
   1000             actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData));
   1001         } else if (MetadataDbHelper.STATUS_AVAILABLE == status) {
   1002             actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData));
   1003         } else {
   1004             Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status);
   1005         }
   1006         actions.execute(context, new LogProblemReporter(TAG));
   1007         signalNewDictionaryState(context);
   1008     }
   1009 
   1010     /**
   1011      * Marks the word list with the passed id as unused.
   1012      *
   1013      * This leaves the file on the disk for ulterior use. The action will see that the destination
   1014      * word list is null, and take appropriate action - in this case, mark it as unused.
   1015      * @see ActionBatch.Action#execute
   1016      *
   1017      * @param context the context for using action batches.
   1018      * @param clientId the id of the client.
   1019      * @param wordlistId the id of the word list to mark as installed.
   1020      * @param version the version of the word list to mark as installed.
   1021      * @param status the current status of the word list.
   1022      */
   1023     // The version and status arguments are not used yet, but this method matches its interface to
   1024     // markAsUsed for consistency.
   1025     public static void markAsUnused(final Context context, final String clientId,
   1026             final String wordlistId, final int version, final int status) {
   1027 
   1028         final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList(
   1029                 context, clientId, wordlistId, version);
   1030 
   1031         if (null == wordListMetaData) return;
   1032         final ActionBatch actions = new ActionBatch();
   1033         actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData));
   1034         actions.execute(context, new LogProblemReporter(TAG));
   1035         signalNewDictionaryState(context);
   1036     }
   1037 
   1038     /**
   1039      * Marks the word list with the passed id as deleting.
   1040      *
   1041      * This basically means that on the next chance there is (right away if Android Keyboard
   1042      * happens to be up, or the next time it gets up otherwise) the dictionary pack will
   1043      * supply an empty dictionary to it that will replace whatever dictionary is installed.
   1044      * This allows to release the space taken by a dictionary (except for the few bytes the
   1045      * empty dictionary takes up), and override a built-in default dictionary so that we
   1046      * can fake delete a built-in dictionary.
   1047      *
   1048      * @param context the context to open the database on.
   1049      * @param clientId the id of the client.
   1050      * @param wordlistId the id of the word list to mark as deleted.
   1051      * @param version the version of the word list to mark as deleted.
   1052      * @param status the current status of the word list.
   1053      */
   1054     public static void markAsDeleting(final Context context, final String clientId,
   1055             final String wordlistId, final int version, final int status) {
   1056 
   1057         final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList(
   1058                 context, clientId, wordlistId, version);
   1059 
   1060         if (null == wordListMetaData) return;
   1061         final ActionBatch actions = new ActionBatch();
   1062         actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData));
   1063         actions.add(new ActionBatch.StartDeleteAction(clientId, wordListMetaData));
   1064         actions.execute(context, new LogProblemReporter(TAG));
   1065         signalNewDictionaryState(context);
   1066     }
   1067 
   1068     /**
   1069      * Marks the word list with the passed id as actually deleted.
   1070      *
   1071      * This reverts to available status or deletes the row as appropriate.
   1072      *
   1073      * @param context the context to open the database on.
   1074      * @param clientId the id of the client.
   1075      * @param wordlistId the id of the word list to mark as deleted.
   1076      * @param version the version of the word list to mark as deleted.
   1077      * @param status the current status of the word list.
   1078      */
   1079     public static void markAsDeleted(final Context context, final String clientId,
   1080             final String wordlistId, final int version, final int status) {
   1081         final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList(
   1082                         context, clientId, wordlistId, version);
   1083 
   1084         if (null == wordListMetaData) return;
   1085 
   1086         final ActionBatch actions = new ActionBatch();
   1087         actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData));
   1088         actions.execute(context, new LogProblemReporter(TAG));
   1089         signalNewDictionaryState(context);
   1090     }
   1091 
   1092     /**
   1093      * Checks whether the word list should be downloaded again; in which case an download &
   1094      * installation attempt is made. Otherwise the word list is marked broken.
   1095      *
   1096      * @param context the context to open the database on.
   1097      * @param clientId the id of the client.
   1098      * @param wordlistId the id of the word list which is broken.
   1099      * @param version the version of the broken word list.
   1100      */
   1101     public static void markAsBrokenOrRetrying(final Context context, final String clientId,
   1102             final String wordlistId, final int version) {
   1103         boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying(
   1104                 MetadataDbHelper.getDb(context, clientId), wordlistId, version);
   1105 
   1106         if (isRetryPossible) {
   1107             if (DEBUG) {
   1108                 Log.d(TAG, "Attempting to download & install the wordlist again.");
   1109             }
   1110             final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList(
   1111                     context, clientId, wordlistId, version);
   1112             if (wordListMetaData == null) {
   1113                 return;
   1114             }
   1115 
   1116             final ActionBatch actions = new ActionBatch();
   1117             actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData));
   1118             actions.execute(context, new LogProblemReporter(TAG));
   1119         } else {
   1120             if (DEBUG) {
   1121                 Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table.");
   1122             }
   1123             MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId),
   1124                     wordlistId, version);
   1125         }
   1126     }
   1127 }
   1128