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