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