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