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