aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
diff options
context:
space:
mode:
authorAmin Bandali <bandali@kelar.org>2024-12-16 21:45:41 -0500
committerAmin Bandali <bandali@kelar.org>2025-01-11 14:17:35 -0500
commite9a0e66716dab4dd3184d009d8920de1961efdfa (patch)
tree02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
parentfb3b9360d70596d7e921de8bf7d3ca99564a077e (diff)
downloadlatinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java')
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java1083
1 files changed, 0 insertions, 1083 deletions
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
deleted file mode 100644
index bdea3e919..000000000
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ /dev/null
@@ -1,1083 +0,0 @@
-/*
- * 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.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.provider.Settings;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.latin.R;
-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);
- }
-
- /**
- * 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. During setup wizard, however, this UI is
- // suppressed.
- final boolean deviceProvisioned = Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.DEVICE_PROVISIONED, 0) != 0;
- if (deviceProvisioned) {
- 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);
- } else {
- Log.i(TAG, "installIfNeverRequested() : Don't show download toast");
- }
-
- 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);
- }
- }
-}