| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| package com.android.inputmethod.dictionarypack; |
| |
| import android.app.DownloadManager; |
| import android.app.DownloadManager.Query; |
| import android.app.DownloadManager.Request; |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.net.ConnectivityManager; |
| import android.net.Uri; |
| import android.os.ParcelFileDescriptor; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.inputmethod.compat.ConnectivityManagerCompatUtils; |
| import com.android.inputmethod.compat.NotificationCompatUtils; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.common.LocaleUtils; |
| import com.android.inputmethod.latin.makedict.FormatSpec; |
| import com.android.inputmethod.latin.utils.ApplicationUtils; |
| import com.android.inputmethod.latin.utils.DebugLogUtils; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.nio.channels.FileChannel; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.TreeSet; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Handler for the update process. |
| * |
| * This class is in charge of coordinating the update process for the various dictionaries |
| * stored in the dictionary pack. |
| */ |
| public final class UpdateHandler { |
| static final String TAG = "DictionaryProvider:" + UpdateHandler.class.getSimpleName(); |
| private static final boolean DEBUG = DictionaryProvider.DEBUG; |
| |
| // Used to prevent trying to read the id of the downloaded file before it is written |
| static final Object sSharedIdProtector = new Object(); |
| |
| // Value used to mean this is not a real DownloadManager downloaded file id |
| // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column |
| // in SQLite, so it should never return anything < 0. |
| public static final int NOT_AN_ID = -1; |
| public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = |
| FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION; |
| |
| // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long. |
| private static final int FILE_COPY_BUFFER_SIZE = 8192; |
| |
| // Table fixed values for metadata / downloads |
| final static String METADATA_NAME = "metadata"; |
| final static int METADATA_TYPE = 0; |
| final static int WORDLIST_TYPE = 1; |
| |
| // Suffix for generated dictionary files |
| private static final String DICT_FILE_SUFFIX = ".dict"; |
| // Name of the category for the main dictionary |
| public static final String MAIN_DICTIONARY_CATEGORY = "main"; |
| |
| public static final String TEMP_DICT_FILE_SUB = "___"; |
| |
| // The id for the "dictionary available" notification. |
| static final int DICT_AVAILABLE_NOTIFICATION_ID = 1; |
| |
| /** |
| * An interface for UIs or services that want to know when something happened. |
| * |
| * This is chiefly used by the dictionary manager UI. |
| */ |
| public interface UpdateEventListener { |
| void downloadedMetadata(boolean succeeded); |
| void wordListDownloadFinished(String wordListId, boolean succeeded); |
| void updateCycleCompleted(); |
| } |
| |
| /** |
| * The list of currently registered listeners. |
| */ |
| private static List<UpdateEventListener> sUpdateEventListeners |
| = Collections.synchronizedList(new LinkedList<UpdateEventListener>()); |
| |
| /** |
| * Register a new listener to be notified of updates. |
| * |
| * Don't forget to call unregisterUpdateEventListener when done with it, or |
| * it will leak the register. |
| */ |
| public static void registerUpdateEventListener(final UpdateEventListener listener) { |
| sUpdateEventListeners.add(listener); |
| } |
| |
| /** |
| * Unregister a previously registered listener. |
| */ |
| public static void unregisterUpdateEventListener(final UpdateEventListener listener) { |
| sUpdateEventListeners.remove(listener); |
| } |
| |
| private static final String DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY = "downloadOverMetered"; |
| |
| /** |
| * Write the DownloadManager ID of the currently downloading metadata to permanent storage. |
| * |
| * @param context to open shared prefs |
| * @param uri the uri of the metadata |
| * @param downloadId the id returned by DownloadManager |
| */ |
| private static void writeMetadataDownloadId(final Context context, final String uri, |
| final long downloadId) { |
| MetadataDbHelper.registerMetadataDownloadId(context, uri, downloadId); |
| } |
| |
| public static final int DOWNLOAD_OVER_METERED_SETTING_UNKNOWN = 0; |
| public static final int DOWNLOAD_OVER_METERED_ALLOWED = 1; |
| public static final int DOWNLOAD_OVER_METERED_DISALLOWED = 2; |
| |
| /** |
| * Sets the setting that tells us whether we may download over a metered connection. |
| */ |
| public static void setDownloadOverMeteredSetting(final Context context, |
| final boolean shouldDownloadOverMetered) { |
| final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); |
| final SharedPreferences.Editor editor = prefs.edit(); |
| editor.putInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, shouldDownloadOverMetered |
| ? DOWNLOAD_OVER_METERED_ALLOWED : DOWNLOAD_OVER_METERED_DISALLOWED); |
| editor.apply(); |
| } |
| |
| /** |
| * Gets the setting that tells us whether we may download over a metered connection. |
| * |
| * This returns one of the constants above. |
| */ |
| public static int getDownloadOverMeteredSetting(final Context context) { |
| final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); |
| final int setting = prefs.getInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, |
| DOWNLOAD_OVER_METERED_SETTING_UNKNOWN); |
| return setting; |
| } |
| |
| /** |
| * Download latest metadata from the server through DownloadManager for all known clients |
| * @param context The context for retrieving resources |
| * @return true if an update successfully started, false otherwise. |
| */ |
| public static boolean tryUpdate(final Context context) { |
| // TODO: loop through all clients instead of only doing the default one. |
| final TreeSet<String> uris = new TreeSet<>(); |
| final Cursor cursor = MetadataDbHelper.queryClientIds(context); |
| if (null == cursor) return false; |
| try { |
| if (!cursor.moveToFirst()) return false; |
| do { |
| final String clientId = cursor.getString(0); |
| final String metadataUri = |
| MetadataDbHelper.getMetadataUriAsString(context, clientId); |
| PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId)); |
| DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri); |
| uris.add(metadataUri); |
| } while (cursor.moveToNext()); |
| } finally { |
| cursor.close(); |
| } |
| boolean started = false; |
| for (final String metadataUri : uris) { |
| if (!TextUtils.isEmpty(metadataUri)) { |
| // If the metadata URI is empty, that means we should never update it at all. |
| // It should not be possible to come here with a null metadata URI, because |
| // it should have been rejected at the time of client registration; if there |
| // is a bug and it happens anyway, doing nothing is the right thing to do. |
| // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}. |
| updateClientsWithMetadataUri(context, metadataUri); |
| started = true; |
| } |
| } |
| return started; |
| } |
| |
| /** |
| * Download latest metadata from the server through DownloadManager for all relevant clients |
| * |
| * @param context The context for retrieving resources |
| * @param metadataUri The client to update |
| */ |
| private static void updateClientsWithMetadataUri( |
| final Context context, final String metadataUri) { |
| Log.i(TAG, "updateClientsWithMetadataUri() : MetadataUri = " + metadataUri); |
| // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. |
| // DownloadManager also stupidly cuts the extension to replace with its own that it |
| // gets from the content-type. We need to circumvent this. |
| final String disambiguator = "#" + System.currentTimeMillis() |
| + ApplicationUtils.getVersionName(context) + ".json"; |
| final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator)); |
| DebugLogUtils.l("Request =", metadataRequest); |
| |
| final Resources res = context.getResources(); |
| metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE); |
| metadataRequest.setTitle(res.getString(R.string.download_description)); |
| // Do not show the notification when downloading the metadata. |
| metadataRequest.setNotificationVisibility(Request.VISIBILITY_HIDDEN); |
| metadataRequest.setVisibleInDownloadsUi( |
| res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); |
| |
| final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); |
| if (maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, |
| DictionaryService.NO_CANCEL_DOWNLOAD_PERIOD_MILLIS)) { |
| // We already have a recent download in progress. Don't register a new download. |
| return; |
| } |
| final long downloadId; |
| synchronized (sSharedIdProtector) { |
| downloadId = manager.enqueue(metadataRequest); |
| DebugLogUtils.l("Metadata download requested with id", downloadId); |
| // If there is still a download in progress, it's been there for a while and |
| // there is probably something wrong with download manager. It's best to just |
| // overwrite the id and request it again. If the old one happens to finish |
| // anyway, we don't know about its ID any more, so the downloadFinished |
| // method will ignore it. |
| writeMetadataDownloadId(context, metadataUri, downloadId); |
| } |
| Log.i(TAG, "updateClientsWithMetadataUri() : DownloadId = " + downloadId); |
| } |
| |
| /** |
| * Cancels downloading a file if there is one for this URI and it's too long. |
| * |
| * If we are not currently downloading the file at this URI, this is a no-op. |
| * |
| * @param context the context to open the database on |
| * @param metadataUri the URI to cancel |
| * @param manager an wrapped instance of DownloadManager |
| * @param graceTime if there was a download started less than this many milliseconds, don't |
| * cancel and return true |
| * @return whether the download is still active |
| */ |
| private static boolean maybeCancelUpdateAndReturnIfStillRunning(final Context context, |
| final String metadataUri, final DownloadManagerWrapper manager, final long graceTime) { |
| synchronized (sSharedIdProtector) { |
| final DownloadIdAndStartDate metadataDownloadIdAndStartDate = |
| MetadataDbHelper.getMetadataDownloadIdAndStartDateForURI(context, metadataUri); |
| if (null == metadataDownloadIdAndStartDate) return false; |
| if (NOT_AN_ID == metadataDownloadIdAndStartDate.mId) return false; |
| if (metadataDownloadIdAndStartDate.mStartDate + graceTime |
| > System.currentTimeMillis()) { |
| return true; |
| } |
| manager.remove(metadataDownloadIdAndStartDate.mId); |
| writeMetadataDownloadId(context, metadataUri, NOT_AN_ID); |
| } |
| // Consider a cancellation as a failure. As such, inform listeners that the download |
| // has failed. |
| for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { |
| listener.downloadedMetadata(false); |
| } |
| return false; |
| } |
| |
| /** |
| * Cancels a pending update for this client, if there is one. |
| * |
| * If we are not currently updating metadata for this client, this is a no-op. This is a helper |
| * method that gets the download manager service and the metadata URI for this client. |
| * |
| * @param context the context, to get an instance of DownloadManager |
| * @param clientId the ID of the client we want to cancel the update of |
| */ |
| public static void cancelUpdate(final Context context, final String clientId) { |
| final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); |
| final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); |
| maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, 0 /* graceTime */); |
| } |
| |
| /** |
| * Registers a download request and flags it as downloading in the metadata table. |
| * |
| * This is a helper method that exists to avoid race conditions where DownloadManager might |
| * finish downloading the file before the data is committed to the database. |
| * It registers the request with the DownloadManager service and also updates the metadata |
| * database directly within a synchronized section. |
| * This method has no intelligence about the data it commits to the database aside from the |
| * download request id, which is not known before submitting the request to the download |
| * manager. Hence, it only updates the relevant line. |
| * |
| * @param manager a wrapped download manager service to register the request with. |
| * @param request the request to register. |
| * @param db the metadata database. |
| * @param id the id of the word list. |
| * @param version the version of the word list. |
| * @return the download id returned by the download manager. |
| */ |
| public static long registerDownloadRequest(final DownloadManagerWrapper manager, |
| final Request request, final SQLiteDatabase db, final String id, final int version) { |
| Log.i(TAG, "registerDownloadRequest() : Id = " + id + " : Version = " + version); |
| final long downloadId; |
| synchronized (sSharedIdProtector) { |
| downloadId = manager.enqueue(request); |
| Log.i(TAG, "registerDownloadRequest() : DownloadId = " + downloadId); |
| MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId); |
| } |
| return downloadId; |
| } |
| |
| /** |
| * Retrieve information about a specific download from DownloadManager. |
| */ |
| private static CompletedDownloadInfo getCompletedDownloadInfo( |
| final DownloadManagerWrapper manager, final long downloadId) { |
| final Query query = new Query().setFilterById(downloadId); |
| final Cursor cursor = manager.query(query); |
| |
| if (null == cursor) { |
| return new CompletedDownloadInfo(null, downloadId, DownloadManager.STATUS_FAILED); |
| } |
| try { |
| final String uri; |
| final int status; |
| if (cursor.moveToNext()) { |
| final int columnStatus = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); |
| final int columnError = cursor.getColumnIndex(DownloadManager.COLUMN_REASON); |
| final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI); |
| final int error = cursor.getInt(columnError); |
| status = cursor.getInt(columnStatus); |
| final String uriWithAnchor = cursor.getString(columnUri); |
| int anchorIndex = uriWithAnchor.indexOf('#'); |
| if (anchorIndex != -1) { |
| uri = uriWithAnchor.substring(0, anchorIndex); |
| } else { |
| uri = uriWithAnchor; |
| } |
| if (DownloadManager.STATUS_SUCCESSFUL != status) { |
| Log.e(TAG, "Permanent failure of download " + downloadId |
| + " with error code: " + error); |
| } |
| } else { |
| uri = null; |
| status = DownloadManager.STATUS_FAILED; |
| } |
| return new CompletedDownloadInfo(uri, downloadId, status); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| private static ArrayList<DownloadRecord> getDownloadRecordsForCompletedDownloadInfo( |
| final Context context, final CompletedDownloadInfo downloadInfo) { |
| // Get and check the ID of the file we are waiting for, compare them to downloaded ones |
| synchronized(sSharedIdProtector) { |
| final ArrayList<DownloadRecord> downloadRecords = |
| MetadataDbHelper.getDownloadRecordsForDownloadId(context, |
| downloadInfo.mDownloadId); |
| // If any of these is metadata, we should update the DB |
| boolean hasMetadata = false; |
| for (DownloadRecord record : downloadRecords) { |
| if (record.isMetadata()) { |
| hasMetadata = true; |
| break; |
| } |
| } |
| if (hasMetadata) { |
| writeMetadataDownloadId(context, downloadInfo.mUri, NOT_AN_ID); |
| MetadataDbHelper.saveLastUpdateTimeOfUri(context, downloadInfo.mUri); |
| } |
| return downloadRecords; |
| } |
| } |
| |
| /** |
| * Take appropriate action after a download finished, in success or in error. |
| * |
| * This is called by the system upon broadcast from the DownloadManager that a file |
| * has been downloaded successfully. |
| * After a simple check that this is actually the file we are waiting for, this |
| * method basically coordinates the parsing and comparison of metadata, and fires |
| * the computation of the list of actions that should be taken then executes them. |
| * |
| * @param context The context for this action. |
| * @param intent The intent from the DownloadManager containing details about the download. |
| */ |
| /* package */ static void downloadFinished(final Context context, final Intent intent) { |
| // Get and check the ID of the file that was downloaded |
| final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID); |
| Log.i(TAG, "downloadFinished() : DownloadId = " + fileId); |
| if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore |
| |
| final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); |
| final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId); |
| |
| final ArrayList<DownloadRecord> recordList = |
| getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo); |
| if (null == recordList) return; // It was someone else's download. |
| DebugLogUtils.l("Received result for download ", fileId); |
| |
| // TODO: handle gracefully a null pointer here. This is practically impossible because |
| // we come here only when DownloadManager explicitly called us when it ended a |
| // download, so we are pretty sure it's alive. It's theoretically possible that it's |
| // disabled right inbetween the firing of the intent and the control reaching here. |
| |
| for (final DownloadRecord record : recordList) { |
| // downloadSuccessful is not final because we may still have exceptions from now on |
| boolean downloadSuccessful = false; |
| try { |
| if (downloadInfo.wasSuccessful()) { |
| downloadSuccessful = handleDownloadedFile(context, record, manager, fileId); |
| Log.i(TAG, "downloadFinished() : Success = " + downloadSuccessful); |
| } |
| } finally { |
| final String resultMessage = downloadSuccessful ? "Success" : "Failure"; |
| if (record.isMetadata()) { |
| Log.i(TAG, "downloadFinished() : Metadata " + resultMessage); |
| publishUpdateMetadataCompleted(context, downloadSuccessful); |
| } else { |
| Log.i(TAG, "downloadFinished() : WordList " + resultMessage); |
| final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId); |
| publishUpdateWordListCompleted(context, downloadSuccessful, fileId, |
| db, record.mAttributes, record.mClientId); |
| } |
| } |
| } |
| // Now that we're done using it, we can remove this download from DLManager |
| manager.remove(fileId); |
| } |
| |
| /** |
| * Sends a broadcast informing listeners that the dictionaries were updated. |
| * |
| * This will call all local listeners through the UpdateEventListener#downloadedMetadata |
| * callback (for example, the dictionary provider interface uses this to stop the Loading |
| * animation) and send a broadcast about the metadata having been updated. For a client of |
| * the dictionary pack like Latin IME, this means it should re-query the dictionary pack |
| * for any relevant new data. |
| * |
| * @param context the context, to send the broadcast. |
| * @param downloadSuccessful whether the download of the metadata was successful or not. |
| */ |
| public static void publishUpdateMetadataCompleted(final Context context, |
| final boolean downloadSuccessful) { |
| // We need to warn all listeners of what happened. But some listeners may want to |
| // remove themselves or re-register something in response. Hence we should take a |
| // snapshot of the listener list and warn them all. This also prevents any |
| // concurrent modification problem of the static list. |
| for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { |
| listener.downloadedMetadata(downloadSuccessful); |
| } |
| publishUpdateCycleCompletedEvent(context); |
| } |
| |
| private static void publishUpdateWordListCompleted(final Context context, |
| final boolean downloadSuccessful, final long fileId, |
| final SQLiteDatabase db, final ContentValues downloadedFileRecord, |
| final String clientId) { |
| synchronized(sSharedIdProtector) { |
| if (downloadSuccessful) { |
| final ActionBatch actions = new ActionBatch(); |
| actions.add(new ActionBatch.InstallAfterDownloadAction(clientId, |
| downloadedFileRecord)); |
| actions.execute(context, new LogProblemReporter(TAG)); |
| } else { |
| MetadataDbHelper.deleteDownloadingEntry(db, fileId); |
| } |
| } |
| // See comment above about #linkedCopyOfLists |
| for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { |
| listener.wordListDownloadFinished(downloadedFileRecord.getAsString( |
| MetadataDbHelper.WORDLISTID_COLUMN), downloadSuccessful); |
| } |
| publishUpdateCycleCompletedEvent(context); |
| } |
| |
| private static void publishUpdateCycleCompletedEvent(final Context context) { |
| // Even if this is not successful, we have to publish the new state. |
| PrivateLog.log("Publishing update cycle completed event"); |
| DebugLogUtils.l("Publishing update cycle completed event"); |
| for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { |
| listener.updateCycleCompleted(); |
| } |
| signalNewDictionaryState(context); |
| } |
| |
| private static boolean handleDownloadedFile(final Context context, |
| final DownloadRecord downloadRecord, final DownloadManagerWrapper manager, |
| final long fileId) { |
| try { |
| // {@link handleWordList(Context,InputStream,ContentValues)}. |
| // Handle the downloaded file according to its type |
| if (downloadRecord.isMetadata()) { |
| DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId); |
| // #handleMetadata() closes its InputStream argument |
| handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream( |
| manager.openDownloadedFile(fileId)), downloadRecord.mClientId); |
| } else { |
| DebugLogUtils.l("Data D/L'd is a word list"); |
| final int wordListStatus = downloadRecord.mAttributes.getAsInteger( |
| MetadataDbHelper.STATUS_COLUMN); |
| if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) { |
| // #handleWordList() closes its InputStream argument |
| handleWordList(context, new ParcelFileDescriptor.AutoCloseInputStream( |
| manager.openDownloadedFile(fileId)), downloadRecord); |
| } else { |
| Log.e(TAG, "Spurious download ended. Maybe a cancelled download?"); |
| } |
| } |
| return true; |
| } catch (FileNotFoundException e) { |
| Log.e(TAG, "A file was downloaded but it can't be opened", e); |
| } catch (IOException e) { |
| // Can't read the file... disk damage? |
| Log.e(TAG, "Can't read a file", e); |
| // TODO: Check with UX how we should warn the user. |
| } catch (IllegalStateException e) { |
| // The format of the downloaded file is incorrect. We should maybe report upstream? |
| Log.e(TAG, "Incorrect data received", e); |
| } catch (BadFormatException e) { |
| // The format of the downloaded file is incorrect. We should maybe report upstream? |
| Log.e(TAG, "Incorrect data received", e); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a copy of the specified list, with all elements copied. |
| * |
| * This returns a linked list. |
| */ |
| private static <T> List<T> linkedCopyOfList(final List<T> src) { |
| // Instantiation of a parameterized type is not possible in Java, so it's not possible to |
| // return the same type of list that was passed - probably the same reason why Collections |
| // does not do it. So we need to decide statically which concrete type to return. |
| return new LinkedList<>(src); |
| } |
| |
| /** |
| * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. |
| */ |
| private static void signalNewDictionaryState(final Context context) { |
| // TODO: Also provide the locale of the updated dictionary so that the LatinIme |
| // does not have to reset if it is a different locale. |
| final Intent newDictBroadcast = |
| new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); |
| context.sendBroadcast(newDictBroadcast); |
| } |
| |
| /** |
| * Parse metadata and take appropriate action (that is, upgrade dictionaries). |
| * @param context the context to read settings. |
| * @param stream an input stream pointing to the downloaded data. May not be null. |
| * Will be closed upon finishing. |
| * @param clientId the ID of the client to update |
| * @throws BadFormatException if the metadata is not in a known format. |
| * @throws IOException if the downloaded file can't be read from the disk |
| */ |
| public static void handleMetadata(final Context context, final InputStream stream, |
| final String clientId) throws IOException, BadFormatException { |
| DebugLogUtils.l("Entering handleMetadata"); |
| final List<WordListMetadata> newMetadata; |
| final InputStreamReader reader = new InputStreamReader(stream); |
| try { |
| // According to the doc InputStreamReader buffers, so no need to add a buffering layer |
| newMetadata = MetadataHandler.readMetadata(reader); |
| } finally { |
| reader.close(); |
| } |
| |
| DebugLogUtils.l("Downloaded metadata :", newMetadata); |
| PrivateLog.log("Downloaded metadata\n" + newMetadata); |
| |
| final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata); |
| // TODO: Check with UX how we should report to the user |
| // TODO: add an action to close the database |
| actions.execute(context, new LogProblemReporter(TAG)); |
| } |
| |
| /** |
| * Handle a word list: put it in its right place, and update the passed content values. |
| * @param context the context for opening files. |
| * @param inputStream an input stream pointing to the downloaded data. May not be null. |
| * Will be closed upon finishing. |
| * @param downloadRecord the content values to fill the file name in. |
| * @throws IOException if files can't be read or written. |
| * @throws BadFormatException if the md5 checksum doesn't match the metadata. |
| */ |
| private static void handleWordList(final Context context, |
| final InputStream inputStream, final DownloadRecord downloadRecord) |
| throws IOException, BadFormatException { |
| |
| // DownloadManager does not have the ability to put the file directly where we want |
| // it, so we had it download to a temporary place. Now we move it. It will be deleted |
| // automatically by DownloadManager. |
| DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString( |
| MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId); |
| PrivateLog.log("Downloaded a new word list with description : " |
| + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN) |
| + " for " + downloadRecord.mClientId); |
| |
| final String locale = |
| downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN); |
| final String destinationFile = getTempFileName(context, locale); |
| downloadRecord.mAttributes.put(MetadataDbHelper.LOCAL_FILENAME_COLUMN, destinationFile); |
| |
| FileOutputStream outputStream = null; |
| try { |
| outputStream = context.openFileOutput(destinationFile, Context.MODE_PRIVATE); |
| copyFile(inputStream, outputStream); |
| } finally { |
| inputStream.close(); |
| if (outputStream != null) { |
| outputStream.close(); |
| } |
| } |
| |
| // TODO: Consolidate this MD5 calculation with file copying above. |
| // We need to reopen the file because the inputstream bytes have been consumed, and there |
| // is nothing in InputStream to reopen or rewind the stream |
| FileInputStream copiedFile = null; |
| final String md5sum; |
| try { |
| copiedFile = context.openFileInput(destinationFile); |
| md5sum = MD5Calculator.checksum(copiedFile); |
| } finally { |
| if (copiedFile != null) { |
| copiedFile.close(); |
| } |
| } |
| if (TextUtils.isEmpty(md5sum)) { |
| return; // We can't compute the checksum anyway, so return and hope for the best |
| } |
| if (!md5sum.equals(downloadRecord.mAttributes.getAsString( |
| MetadataDbHelper.CHECKSUM_COLUMN))) { |
| context.deleteFile(destinationFile); |
| throw new BadFormatException("MD5 checksum check failed : \"" + md5sum + "\" <> \"" |
| + downloadRecord.mAttributes.getAsString(MetadataDbHelper.CHECKSUM_COLUMN) |
| + "\""); |
| } |
| } |
| |
| /** |
| * Copies in to out using FileChannels. |
| * |
| * This tries to use channels for fast copying. If it doesn't work, fall back to |
| * copyFileFallBack below. |
| * |
| * @param in the stream to copy from. |
| * @param out the stream to copy to. |
| * @throws IOException if both the normal and fallback methods raise exceptions. |
| */ |
| private static void copyFile(final InputStream in, final OutputStream out) |
| throws IOException { |
| DebugLogUtils.l("Copying files"); |
| if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) { |
| DebugLogUtils.l("Not the right types"); |
| copyFileFallback(in, out); |
| } else { |
| try { |
| final FileChannel sourceChannel = ((FileInputStream) in).getChannel(); |
| final FileChannel destinationChannel = ((FileOutputStream) out).getChannel(); |
| sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel); |
| } catch (IOException e) { |
| // Can't work with channels, or something went wrong. Copy by hand. |
| DebugLogUtils.l("Won't work"); |
| copyFileFallback(in, out); |
| } |
| } |
| } |
| |
| /** |
| * Copies in to out with read/write methods, not FileChannels. |
| * |
| * @param in the stream to copy from. |
| * @param out the stream to copy to. |
| * @throws IOException if a read or a write fails. |
| */ |
| private static void copyFileFallback(final InputStream in, final OutputStream out) |
| throws IOException { |
| DebugLogUtils.l("Falling back to slow copy"); |
| final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE]; |
| for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer)) |
| out.write(buffer, 0, readBytes); |
| } |
| |
| /** |
| * Creates and returns a new file to store a dictionary |
| * @param context the context to use to open the file. |
| * @param locale the locale for this dictionary, to make the file name more readable. |
| * @return the file name, or throw an exception. |
| * @throws IOException if the file cannot be created. |
| */ |
| private static String getTempFileName(final Context context, final String locale) |
| throws IOException { |
| DebugLogUtils.l("Entering openTempFileOutput"); |
| final File dir = context.getFilesDir(); |
| final File f = File.createTempFile(locale + TEMP_DICT_FILE_SUB, DICT_FILE_SUFFIX, dir); |
| DebugLogUtils.l("File name is", f.getName()); |
| return f.getName(); |
| } |
| |
| /** |
| * Compare metadata (collections of word lists). |
| * |
| * This method takes whole metadata sets directly and compares them, matching the wordlists in |
| * each of them on the id. It creates an ActionBatch object that can be .execute()'d to perform |
| * the actual upgrade from `from' to `to'. |
| * |
| * @param context the context to open databases on. |
| * @param clientId the id of the client. |
| * @param from the dictionary descriptor (as a list of wordlists) to upgrade from. |
| * @param to the dictionary descriptor (as a list of wordlists) to upgrade to. |
| * @return an ordered list of runnables to be called to upgrade. |
| */ |
| private static ActionBatch compareMetadataForUpgrade(final Context context, |
| final String clientId, @Nullable final List<WordListMetadata> from, |
| @Nullable final List<WordListMetadata> to) { |
| final ActionBatch actions = new ActionBatch(); |
| // Upgrade existing word lists |
| DebugLogUtils.l("Comparing dictionaries"); |
| final Set<String> wordListIds = new TreeSet<>(); |
| // TODO: Can these be null? |
| final List<WordListMetadata> fromList = (from == null) ? new ArrayList<WordListMetadata>() |
| : from; |
| final List<WordListMetadata> toList = (to == null) ? new ArrayList<WordListMetadata>() |
| : to; |
| for (WordListMetadata wlData : fromList) wordListIds.add(wlData.mId); |
| for (WordListMetadata wlData : toList) wordListIds.add(wlData.mId); |
| for (String id : wordListIds) { |
| final WordListMetadata currentInfo = MetadataHandler.findWordListById(fromList, id); |
| final WordListMetadata metadataInfo = MetadataHandler.findWordListById(toList, id); |
| // TODO: Remove the following unnecessary check, since we are now doing the filtering |
| // inside findWordListById. |
| final WordListMetadata newInfo = null == metadataInfo |
| || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION |
| ? null : metadataInfo; |
| DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo); |
| |
| if (null == currentInfo && null == newInfo) { |
| // This may happen if a new word list appeared that we can't handle. |
| if (null == metadataInfo) { |
| // What happened? Bug in Set<>? |
| Log.e(TAG, "Got an id for a wordlist that is neither in from nor in to"); |
| } else { |
| // We may come here if there is a new word list that we can't handle. |
| Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format" |
| + " version " + metadataInfo.mFormatVersion + " and the maximum version" |
| + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION); |
| } |
| continue; |
| } else if (null == currentInfo) { |
| // This is the case where a new list that we did not know of popped on the server. |
| // Make it available. |
| actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); |
| } else if (null == newInfo) { |
| // This is the case where an old list we had is not in the server data any more. |
| // Pass false to ForgetAction: this may be installed and we still want to apply |
| // a forget-like action (remove the URL) if it is, so we want to turn off the |
| // status == AVAILABLE check. If it's DELETING, this is the right thing to do, |
| // as we want to leave the record as long as Android Keyboard has not deleted it ; |
| // the record will be removed when the file is actually deleted. |
| actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, false)); |
| } else { |
| final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); |
| if (newInfo.mVersion == currentInfo.mVersion) { |
| if (TextUtils.equals(newInfo.mRemoteFilename, currentInfo.mRemoteFilename)) { |
| // If the dictionary url hasn't changed, we should preserve the retryCount. |
| newInfo.mRetryCount = currentInfo.mRetryCount; |
| } |
| // If it's the same id/version, we update the DB with the new values. |
| // It doesn't matter too much if they didn't change. |
| actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); |
| } else if (newInfo.mVersion > currentInfo.mVersion) { |
| // If it's a new version, it's a different entry in the database. Make it |
| // available, and if it's installed, also start the download. |
| final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, |
| currentInfo.mId, currentInfo.mVersion); |
| final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); |
| actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); |
| if (status == MetadataDbHelper.STATUS_INSTALLED |
| || status == MetadataDbHelper.STATUS_DISABLED) { |
| actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo)); |
| } else { |
| // Pass true to ForgetAction: this is indeed an update to a non-installed |
| // word list, so activate status == AVAILABLE check |
| // In case the status is DELETING, this is the right thing to do. It will |
| // leave the entry as DELETING and remove its URL so that Android Keyboard |
| // can delete it the next time it starts up. |
| actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, true)); |
| } |
| } else if (DEBUG) { |
| Log.i(TAG, "Not updating word list " + id |
| + " : current list timestamp is " + currentInfo.mLastUpdate |
| + " ; new list timestamp is " + newInfo.mLastUpdate); |
| } |
| } |
| } |
| return actions; |
| } |
| |
| /** |
| * Computes an upgrade from the current state of the dictionaries to some desired state. |
| * @param context the context for reading settings and files. |
| * @param clientId the id of the client. |
| * @param newMetadata the state we want to upgrade to. |
| * @return the upgrade from the current state to the desired state, ready to be executed. |
| */ |
| public static ActionBatch computeUpgradeTo(final Context context, final String clientId, |
| final List<WordListMetadata> newMetadata) { |
| final List<WordListMetadata> currentMetadata = |
| MetadataHandler.getCurrentMetadata(context, clientId); |
| return compareMetadataForUpgrade(context, clientId, currentMetadata, newMetadata); |
| } |
| |
| /** |
| * Shows the notification that informs the user a dictionary is available. |
| * |
| * When this notification is clicked, the dialog for downloading the dictionary |
| * over a metered connection is shown. |
| */ |
| private static void showDictionaryAvailableNotification(final Context context, |
| final String clientId, final ContentValues installCandidate) { |
| final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); |
| final Intent intent = new Intent(); |
| intent.setClass(context, DownloadOverMeteredDialog.class); |
| intent.putExtra(DownloadOverMeteredDialog.CLIENT_ID_KEY, clientId); |
| intent.putExtra(DownloadOverMeteredDialog.WORDLIST_TO_DOWNLOAD_KEY, |
| installCandidate.getAsString(MetadataDbHelper.WORDLISTID_COLUMN)); |
| intent.putExtra(DownloadOverMeteredDialog.SIZE_KEY, |
| installCandidate.getAsInteger(MetadataDbHelper.FILESIZE_COLUMN)); |
| intent.putExtra(DownloadOverMeteredDialog.LOCALE_KEY, localeString); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
| final PendingIntent notificationIntent = PendingIntent.getActivity(context, |
| 0 /* requestCode */, intent, |
| PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT); |
| final NotificationManager notificationManager = |
| (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); |
| // None of those are expected to happen, but just in case... |
| if (null == notificationIntent || null == notificationManager) return; |
| |
| final String language = (null == localeString) ? "" |
| : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage(); |
| final String titleFormat = context.getString(R.string.dict_available_notification_title); |
| final String notificationTitle = String.format(titleFormat, language); |
| final Notification.Builder builder = new Notification.Builder(context) |
| .setAutoCancel(true) |
| .setContentIntent(notificationIntent) |
| .setContentTitle(notificationTitle) |
| .setContentText(context.getString(R.string.dict_available_notification_description)) |
| .setTicker(notificationTitle) |
| .setOngoing(false) |
| .setOnlyAlertOnce(true) |
| .setSmallIcon(R.drawable.ic_notify_dictionary); |
| NotificationCompatUtils.setColor(builder, |
| context.getResources().getColor(R.color.notification_accent_color)); |
| NotificationCompatUtils.setPriorityToLow(builder); |
| NotificationCompatUtils.setVisibilityToSecret(builder); |
| NotificationCompatUtils.setCategoryToRecommendation(builder); |
| final Notification notification = NotificationCompatUtils.build(builder); |
| notificationManager.notify(DICT_AVAILABLE_NOTIFICATION_ID, notification); |
| } |
| |
| /** |
| * Installs a word list if it has never been requested. |
| * |
| * This is called when a word list is requested, and is available but not installed. It checks |
| * the conditions for auto-installation: if the dictionary is a main dictionary for this |
| * language, and it has never been opted out through the dictionary interface, then we start |
| * installing it. For the user who enables a language and uses it for the first time, the |
| * dictionary should magically start being used a short time after they start typing. |
| * The mayPrompt argument indicates whether we should prompt the user for a decision to |
| * download or not, in case we decide we are in the case where we should download - this |
| * roughly happens when the current connectivity is 3G. See |
| * DictionaryProvider#getDictionaryWordListsForContentUri for details. |
| */ |
| // As opposed to many other methods, this method does not need the version of the word |
| // list because it may only install the latest version we know about for this specific |
| // word list ID / client ID combination. |
| public static void installIfNeverRequested(final Context context, final String clientId, |
| final String wordlistId) { |
| Log.i(TAG, "installIfNeverRequested() : ClientId = " + clientId |
| + " : WordListId = " + wordlistId); |
| final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR); |
| // If we have a new-format dictionary id (category:manual_id), then use the |
| // specified category. Otherwise, it is a main dictionary, so force the |
| // MAIN category upon it. |
| final String category = 2 == idArray.length ? idArray[0] : MAIN_DICTIONARY_CATEGORY; |
| if (!MAIN_DICTIONARY_CATEGORY.equals(category)) { |
| // Not a main dictionary. We only auto-install main dictionaries, so we can return now. |
| return; |
| } |
| if (CommonPreferences.getCommonPreferences(context).contains(wordlistId)) { |
| // If some kind of settings has been done in the past for this specific id, then |
| // this is not a candidate for auto-install. Because it already is either true, |
| // in which case it may be installed or downloading or whatever, and we don't |
| // need to care about it because it's already handled or being handled, or it's false |
| // in which case it means the user explicitely turned it off and don't want to have |
| // it installed. So we quit right away. |
| return; |
| } |
| |
| final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); |
| final ContentValues installCandidate = |
| MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId); |
| if (MetadataDbHelper.STATUS_AVAILABLE |
| != installCandidate.getAsInteger(MetadataDbHelper.STATUS_COLUMN)) { |
| // If it's not "AVAILABLE", we want to stop now. Because candidates for auto-install |
| // are lists that we know are available, but we also know have never been installed. |
| // It does obviously not concern already installed lists, or downloading lists, |
| // or those that have been disabled, flagged as deleting... So anything else than |
| // AVAILABLE means we don't auto-install. |
| return; |
| } |
| |
| // We decided against prompting the user for a decision. This may be because we were |
| // explicitly asked not to, or because we are currently on wi-fi anyway, or because we |
| // already know the answer to the question. We'll enqueue a request ; StartDownloadAction |
| // knows to use the correct type of network according to the current settings. |
| |
| // Also note that once it's auto-installed, a word list will be marked as INSTALLED. It will |
| // thus receive automatic updates if there are any, which is what we want. If the user does |
| // not want this word list, they will have to go to the settings and change them, which will |
| // change the shared preferences. So there is no way for a word list that has been |
| // auto-installed once to get auto-installed again, and that's what we want. |
| final ActionBatch actions = new ActionBatch(); |
| WordListMetadata metadata = WordListMetadata.createFromContentValues(installCandidate); |
| actions.add(new ActionBatch.StartDownloadAction(clientId, metadata)); |
| final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); |
| // We are in a content provider: we can't do any UI at all. We have to defer the displaying |
| // itself to the service. Also, we only display this when the user does not have a |
| // dictionary for this language already. |
| final Intent intent = new Intent(); |
| intent.setClass(context, DictionaryService.class); |
| intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION); |
| intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString); |
| context.startService(intent); |
| Log.i(TAG, "installIfNeverRequested() : StartDownloadAction for " + metadata); |
| actions.execute(context, new LogProblemReporter(TAG)); |
| } |
| |
| /** |
| * Marks the word list with the passed id as used. |
| * |
| * This will download/install the list as required. The action will see that the destination |
| * word list is a valid list, and take appropriate action - in this case, mark it as used. |
| * @see ActionBatch.Action#execute |
| * |
| * @param context the context for using action batches. |
| * @param clientId the id of the client. |
| * @param wordlistId the id of the word list to mark as installed. |
| * @param version the version of the word list to mark as installed. |
| * @param status the current status of the word list. |
| * @param allowDownloadOnMeteredData whether to download even on metered data connection |
| */ |
| // The version argument is not used yet, because we don't need it to retrieve the information |
| // we need. However, the pair (id, version) being the primary key to a word list in the database |
| // it feels better for consistency to pass it, and some methods retrieving information about a |
| // word list need it so we may need it in the future. |
| public static void markAsUsed(final Context context, final String clientId, |
| final String wordlistId, final int version, |
| final int status, final boolean allowDownloadOnMeteredData) { |
| final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( |
| context, clientId, wordlistId, version); |
| |
| if (null == wordListMetaData) return; |
| |
| final ActionBatch actions = new ActionBatch(); |
| if (MetadataDbHelper.STATUS_DISABLED == status |
| || MetadataDbHelper.STATUS_DELETING == status) { |
| actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); |
| } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { |
| actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); |
| } else { |
| Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); |
| } |
| actions.execute(context, new LogProblemReporter(TAG)); |
| signalNewDictionaryState(context); |
| } |
| |
| /** |
| * Marks the word list with the passed id as unused. |
| * |
| * This leaves the file on the disk for ulterior use. The action will see that the destination |
| * word list is null, and take appropriate action - in this case, mark it as unused. |
| * @see ActionBatch.Action#execute |
| * |
| * @param context the context for using action batches. |
| * @param clientId the id of the client. |
| * @param wordlistId the id of the word list to mark as installed. |
| * @param version the version of the word list to mark as installed. |
| * @param status the current status of the word list. |
| */ |
| // The version and status arguments are not used yet, but this method matches its interface to |
| // markAsUsed for consistency. |
| public static void markAsUnused(final Context context, final String clientId, |
| final String wordlistId, final int version, final int status) { |
| |
| final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( |
| context, clientId, wordlistId, version); |
| |
| if (null == wordListMetaData) return; |
| final ActionBatch actions = new ActionBatch(); |
| actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); |
| actions.execute(context, new LogProblemReporter(TAG)); |
| signalNewDictionaryState(context); |
| } |
| |
| /** |
| * Marks the word list with the passed id as deleting. |
| * |
| * This basically means that on the next chance there is (right away if Android Keyboard |
| * happens to be up, or the next time it gets up otherwise) the dictionary pack will |
| * supply an empty dictionary to it that will replace whatever dictionary is installed. |
| * This allows to release the space taken by a dictionary (except for the few bytes the |
| * empty dictionary takes up), and override a built-in default dictionary so that we |
| * can fake delete a built-in dictionary. |
| * |
| * @param context the context to open the database on. |
| * @param clientId the id of the client. |
| * @param wordlistId the id of the word list to mark as deleted. |
| * @param version the version of the word list to mark as deleted. |
| * @param status the current status of the word list. |
| */ |
| public static void markAsDeleting(final Context context, final String clientId, |
| final String wordlistId, final int version, final int status) { |
| |
| final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( |
| context, clientId, wordlistId, version); |
| |
| if (null == wordListMetaData) return; |
| final ActionBatch actions = new ActionBatch(); |
| actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); |
| actions.add(new ActionBatch.StartDeleteAction(clientId, wordListMetaData)); |
| actions.execute(context, new LogProblemReporter(TAG)); |
| signalNewDictionaryState(context); |
| } |
| |
| /** |
| * Marks the word list with the passed id as actually deleted. |
| * |
| * This reverts to available status or deletes the row as appropriate. |
| * |
| * @param context the context to open the database on. |
| * @param clientId the id of the client. |
| * @param wordlistId the id of the word list to mark as deleted. |
| * @param version the version of the word list to mark as deleted. |
| * @param status the current status of the word list. |
| */ |
| public static void markAsDeleted(final Context context, final String clientId, |
| final String wordlistId, final int version, final int status) { |
| final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( |
| context, clientId, wordlistId, version); |
| |
| if (null == wordListMetaData) return; |
| |
| final ActionBatch actions = new ActionBatch(); |
| actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData)); |
| actions.execute(context, new LogProblemReporter(TAG)); |
| signalNewDictionaryState(context); |
| } |
| |
| /** |
| * Checks whether the word list should be downloaded again; in which case an download & |
| * installation attempt is made. Otherwise the word list is marked broken. |
| * |
| * @param context the context to open the database on. |
| * @param clientId the id of the client. |
| * @param wordlistId the id of the word list which is broken. |
| * @param version the version of the broken word list. |
| */ |
| public static void markAsBrokenOrRetrying(final Context context, final String clientId, |
| final String wordlistId, final int version) { |
| boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying( |
| MetadataDbHelper.getDb(context, clientId), wordlistId, version); |
| |
| if (isRetryPossible) { |
| if (DEBUG) { |
| Log.d(TAG, "Attempting to download & install the wordlist again."); |
| } |
| final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( |
| context, clientId, wordlistId, version); |
| if (wordListMetaData == null) { |
| return; |
| } |
| |
| final ActionBatch actions = new ActionBatch(); |
| actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); |
| actions.execute(context, new LogProblemReporter(TAG)); |
| } else { |
| if (DEBUG) { |
| Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table."); |
| } |
| MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), |
| wordlistId, version); |
| } |
| } |
| } |