2013-03-15 10:00:51 +00:00
|
|
|
/*
|
|
|
|
* 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;
|
2014-08-28 04:03:32 +00:00
|
|
|
import android.os.Build;
|
2013-03-15 10:00:51 +00:00
|
|
|
import android.os.ParcelFileDescriptor;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Log;
|
|
|
|
|
|
|
|
import com.android.inputmethod.compat.ConnectivityManagerCompatUtils;
|
|
|
|
import com.android.inputmethod.compat.DownloadManagerCompatUtils;
|
2014-08-28 04:03:32 +00:00
|
|
|
import com.android.inputmethod.compat.NotificationCompatUtils;
|
2013-03-15 10:00:51 +00:00
|
|
|
import com.android.inputmethod.latin.R;
|
2013-07-05 09:23:22 +00:00
|
|
|
import com.android.inputmethod.latin.utils.ApplicationUtils;
|
2013-06-25 08:03:05 +00:00
|
|
|
import com.android.inputmethod.latin.utils.DebugLogUtils;
|
2013-03-15 10:00:51 +00:00
|
|
|
|
|
|
|
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.Locale;
|
|
|
|
import java.util.Set;
|
|
|
|
import java.util.TreeSet;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 = 2;
|
|
|
|
|
|
|
|
// 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";
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
public void downloadedMetadata(boolean succeeded);
|
|
|
|
public void wordListDownloadFinished(String wordListId, boolean succeeded);
|
|
|
|
public 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
|
|
|
|
* @param updateNow Whether we should update NOW, or respect bandwidth policies
|
2013-06-28 04:06:59 +00:00
|
|
|
* @return true if an update successfully started, false otherwise.
|
2013-03-15 10:00:51 +00:00
|
|
|
*/
|
2013-06-28 04:06:59 +00:00
|
|
|
public static boolean tryUpdate(final Context context, final boolean updateNow) {
|
2013-03-15 10:00:51 +00:00
|
|
|
// TODO: loop through all clients instead of only doing the default one.
|
2014-05-23 11:18:17 +00:00
|
|
|
final TreeSet<String> uris = new TreeSet<>();
|
2013-03-15 10:00:51 +00:00
|
|
|
final Cursor cursor = MetadataDbHelper.queryClientIds(context);
|
2013-06-28 04:06:59 +00:00
|
|
|
if (null == cursor) return false;
|
2013-03-15 10:00:51 +00:00
|
|
|
try {
|
2013-06-28 04:06:59 +00:00
|
|
|
if (!cursor.moveToFirst()) return false;
|
2013-03-15 10:00:51 +00:00
|
|
|
do {
|
|
|
|
final String clientId = cursor.getString(0);
|
|
|
|
final String metadataUri =
|
|
|
|
MetadataDbHelper.getMetadataUriAsString(context, clientId);
|
2013-06-25 08:03:05 +00:00
|
|
|
PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId));
|
|
|
|
DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
|
2013-03-15 10:00:51 +00:00
|
|
|
uris.add(metadataUri);
|
|
|
|
} while (cursor.moveToNext());
|
|
|
|
} finally {
|
|
|
|
cursor.close();
|
|
|
|
}
|
2013-06-28 04:06:59 +00:00
|
|
|
boolean started = false;
|
2013-03-15 10:00:51 +00:00
|
|
|
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, updateNow, metadataUri);
|
2013-06-28 04:06:59 +00:00
|
|
|
started = true;
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
}
|
2013-06-28 04:06:59 +00:00
|
|
|
return started;
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Download latest metadata from the server through DownloadManager for all relevant clients
|
|
|
|
*
|
|
|
|
* @param context The context for retrieving resources
|
|
|
|
* @param updateNow Whether we should update NOW, or respect bandwidth policies
|
|
|
|
* @param metadataUri The client to update
|
|
|
|
*/
|
|
|
|
private static void updateClientsWithMetadataUri(final Context context,
|
|
|
|
final boolean updateNow, final String metadataUri) {
|
2013-06-25 08:03:05 +00:00
|
|
|
PrivateLog.log("Update for metadata URI " + DebugLogUtils.s(metadataUri));
|
2013-04-25 08:16:03 +00:00
|
|
|
// 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()
|
2013-07-05 09:23:22 +00:00
|
|
|
+ ApplicationUtils.getVersionName(context) + ".json";
|
2013-04-25 08:16:03 +00:00
|
|
|
final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Request =", metadataRequest);
|
2013-03-15 10:00:51 +00:00
|
|
|
|
|
|
|
final Resources res = context.getResources();
|
|
|
|
// By default, download over roaming is allowed and all network types are allowed too.
|
|
|
|
if (!updateNow) {
|
|
|
|
final boolean allowedOverMetered = res.getBoolean(R.bool.allow_over_metered);
|
|
|
|
// If we don't have to update NOW, then only do it over non-metered connections.
|
|
|
|
if (DownloadManagerCompatUtils.hasSetAllowedOverMetered()) {
|
|
|
|
DownloadManagerCompatUtils.setAllowedOverMetered(metadataRequest,
|
|
|
|
allowedOverMetered);
|
|
|
|
} else if (!allowedOverMetered) {
|
|
|
|
metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI);
|
|
|
|
}
|
|
|
|
metadataRequest.setAllowedOverRoaming(res.getBoolean(R.bool.allow_over_roaming));
|
|
|
|
}
|
|
|
|
final boolean notificationVisible = updateNow
|
|
|
|
? res.getBoolean(R.bool.display_notification_for_user_requested_update)
|
|
|
|
: res.getBoolean(R.bool.display_notification_for_auto_update);
|
|
|
|
|
|
|
|
metadataRequest.setTitle(res.getString(R.string.download_description));
|
|
|
|
metadataRequest.setNotificationVisibility(notificationVisible
|
|
|
|
? Request.VISIBILITY_VISIBLE : Request.VISIBILITY_HIDDEN);
|
|
|
|
metadataRequest.setVisibleInDownloadsUi(
|
|
|
|
res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI));
|
|
|
|
|
2014-02-17 07:14:18 +00:00
|
|
|
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
2013-03-15 10:00:51 +00:00
|
|
|
cancelUpdateWithDownloadManager(context, metadataUri, manager);
|
|
|
|
final long downloadId;
|
|
|
|
synchronized (sSharedIdProtector) {
|
|
|
|
downloadId = manager.enqueue(metadataRequest);
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Metadata download requested with id", downloadId);
|
2013-03-15 10:00:51 +00:00
|
|
|
// If there is already 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);
|
|
|
|
}
|
2013-04-16 12:44:42 +00:00
|
|
|
PrivateLog.log("Requested download with id " + downloadId);
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-07-10 06:27:26 +00:00
|
|
|
* Cancels downloading a file, if there is one for this URI.
|
2013-03-15 10:00:51 +00:00
|
|
|
*
|
2013-07-10 06:27:26 +00:00
|
|
|
* If we are not currently downloading the file at this URI, this is a no-op.
|
2013-03-15 10:00:51 +00:00
|
|
|
*
|
|
|
|
* @param context the context to open the database on
|
2013-07-10 06:27:26 +00:00
|
|
|
* @param metadataUri the URI to cancel
|
2014-02-17 07:14:18 +00:00
|
|
|
* @param manager an wrapped instance of DownloadManager
|
2013-03-15 10:00:51 +00:00
|
|
|
*/
|
|
|
|
private static void cancelUpdateWithDownloadManager(final Context context,
|
2014-02-17 07:14:18 +00:00
|
|
|
final String metadataUri, final DownloadManagerWrapper manager) {
|
2013-03-15 10:00:51 +00:00
|
|
|
synchronized (sSharedIdProtector) {
|
|
|
|
final long metadataDownloadId =
|
2013-07-10 06:27:26 +00:00
|
|
|
MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri);
|
2013-03-15 10:00:51 +00:00
|
|
|
if (NOT_AN_ID == metadataDownloadId) return;
|
|
|
|
manager.remove(metadataDownloadId);
|
2013-07-10 06:27:26 +00:00
|
|
|
writeMetadataDownloadId(context, metadataUri, NOT_AN_ID);
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
// Consider a cancellation as a failure. As such, inform listeners that the download
|
|
|
|
// has failed.
|
|
|
|
for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
|
|
|
|
listener.downloadedMetadata(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2013-07-10 06:27:26 +00:00
|
|
|
* Cancels a pending update for this client, if there is one.
|
2013-03-15 10:00:51 +00:00
|
|
|
*
|
2013-07-10 06:27:26 +00:00
|
|
|
* 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.
|
2013-03-15 10:00:51 +00:00
|
|
|
*
|
|
|
|
* @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) {
|
2014-02-17 07:14:18 +00:00
|
|
|
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
2013-07-10 06:27:26 +00:00
|
|
|
final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId);
|
2014-02-17 07:14:18 +00:00
|
|
|
cancelUpdateWithDownloadManager(context, metadataUri, manager);
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
2014-02-17 07:14:18 +00:00
|
|
|
* @param manager a wrapped download manager service to register the request with.
|
2013-03-15 10:00:51 +00:00
|
|
|
* @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.
|
|
|
|
*/
|
2014-02-17 07:14:18 +00:00
|
|
|
public static long registerDownloadRequest(final DownloadManagerWrapper manager,
|
|
|
|
final Request request, final SQLiteDatabase db, final String id, final int version) {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
|
2013-03-15 10:00:51 +00:00
|
|
|
final long downloadId;
|
|
|
|
synchronized (sSharedIdProtector) {
|
|
|
|
downloadId = manager.enqueue(request);
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Download requested with id", downloadId);
|
2013-03-15 10:00:51 +00:00
|
|
|
MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId);
|
|
|
|
}
|
|
|
|
return downloadId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve information about a specific download from DownloadManager.
|
|
|
|
*/
|
2014-02-17 07:14:18 +00:00
|
|
|
private static CompletedDownloadInfo getCompletedDownloadInfo(
|
|
|
|
final DownloadManagerWrapper manager, final long downloadId) {
|
2013-03-15 10:00:51 +00:00
|
|
|
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);
|
2013-04-25 08:16:03 +00:00
|
|
|
final String uriWithAnchor = cursor.getString(columnUri);
|
|
|
|
int anchorIndex = uriWithAnchor.indexOf('#');
|
|
|
|
if (anchorIndex != -1) {
|
|
|
|
uri = uriWithAnchor.substring(0, anchorIndex);
|
|
|
|
} else {
|
|
|
|
uri = uriWithAnchor;
|
|
|
|
}
|
2013-03-15 10:00:51 +00:00
|
|
|
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 (null == record.mAttributes) {
|
|
|
|
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);
|
2013-04-16 12:44:42 +00:00
|
|
|
PrivateLog.log("Download finished with id " + fileId);
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("DownloadFinished with id", fileId);
|
2013-03-15 10:00:51 +00:00
|
|
|
if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
|
|
|
|
|
2014-02-17 07:14:18 +00:00
|
|
|
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
2013-03-15 10:00:51 +00:00
|
|
|
final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId);
|
|
|
|
|
|
|
|
final ArrayList<DownloadRecord> recordList =
|
|
|
|
getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo);
|
|
|
|
if (null == recordList) return; // It was someone else's download.
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Received result for download ", fileId);
|
2013-03-15 10:00:51 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
if (record.isMetadata()) {
|
|
|
|
publishUpdateMetadataCompleted(context, downloadSuccessful);
|
|
|
|
} else {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2013-03-28 09:59:19 +00:00
|
|
|
/**
|
|
|
|
* 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,
|
2013-03-15 10:00:51 +00:00
|
|
|
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.
|
2013-04-16 12:44:42 +00:00
|
|
|
PrivateLog.log("Publishing update cycle completed event");
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Publishing update cycle completed event");
|
2013-03-15 10:00:51 +00:00
|
|
|
for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
|
|
|
|
listener.updateCycleCompleted();
|
|
|
|
}
|
|
|
|
signalNewDictionaryState(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static boolean handleDownloadedFile(final Context context,
|
2014-02-17 07:14:18 +00:00
|
|
|
final DownloadRecord downloadRecord, final DownloadManagerWrapper manager,
|
2013-03-15 10:00:51 +00:00
|
|
|
final long fileId) {
|
|
|
|
try {
|
|
|
|
// {@link handleWordList(Context,InputStream,ContentValues)}.
|
|
|
|
// Handle the downloaded file according to its type
|
|
|
|
if (downloadRecord.isMetadata()) {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
|
2013-03-15 10:00:51 +00:00
|
|
|
// #handleMetadata() closes its InputStream argument
|
|
|
|
handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream(
|
|
|
|
manager.openDownloadedFile(fileId)), downloadRecord.mClientId);
|
|
|
|
} else {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Data D/L'd is a word list");
|
2013-03-15 10:00:51 +00:00
|
|
|
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.
|
2014-05-23 11:18:17 +00:00
|
|
|
return new LinkedList<>(src);
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Warn Android Keyboard that the state of dictionaries changed and it should refresh its data.
|
|
|
|
*/
|
|
|
|
private static void signalNewDictionaryState(final Context context) {
|
2013-03-19 07:45:25 +00:00
|
|
|
final Intent newDictBroadcast =
|
|
|
|
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
|
2013-03-15 10:00:51 +00:00
|
|
|
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
|
|
|
|
*/
|
|
|
|
private static void handleMetadata(final Context context, final InputStream stream,
|
|
|
|
final String clientId) throws IOException, BadFormatException {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Entering handleMetadata");
|
2013-03-15 10:00:51 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Downloaded metadata :", newMetadata);
|
2013-04-16 12:44:42 +00:00
|
|
|
PrivateLog.log("Downloaded metadata\n" + newMetadata);
|
2013-03-15 10:00:51 +00:00
|
|
|
|
|
|
|
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.
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
|
2013-03-15 10:00:51 +00:00
|
|
|
MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
|
|
|
|
PrivateLog.log("Downloaded a new word list with description : "
|
|
|
|
+ downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
|
2013-04-16 12:44:42 +00:00
|
|
|
+ " for " + downloadRecord.mClientId);
|
2013-03-15 10:00:51 +00:00
|
|
|
|
|
|
|
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 {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Copying files");
|
2013-03-15 10:00:51 +00:00
|
|
|
if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Not the right types");
|
2013-03-15 10:00:51 +00:00
|
|
|
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.
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Won't work");
|
2013-03-15 10:00:51 +00:00
|
|
|
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 {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Falling back to slow copy");
|
2013-03-15 10:00:51 +00:00
|
|
|
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 {
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Entering openTempFileOutput");
|
2013-03-15 10:00:51 +00:00
|
|
|
final File dir = context.getFilesDir();
|
|
|
|
final File f = File.createTempFile(locale + "___", DICT_FILE_SUFFIX, dir);
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("File name is", f.getName());
|
2013-03-15 10:00:51 +00:00
|
|
|
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, List<WordListMetadata> from, List<WordListMetadata> to) {
|
|
|
|
final ActionBatch actions = new ActionBatch();
|
|
|
|
// Upgrade existing word lists
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Comparing dictionaries");
|
2014-05-23 11:18:17 +00:00
|
|
|
final Set<String> wordListIds = new TreeSet<>();
|
2013-03-15 10:00:51 +00:00
|
|
|
// TODO: Can these be null?
|
2014-05-23 11:18:17 +00:00
|
|
|
if (null == from) from = new ArrayList<>();
|
|
|
|
if (null == to) to = new ArrayList<>();
|
2013-03-15 10:00:51 +00:00
|
|
|
for (WordListMetadata wlData : from) wordListIds.add(wlData.mId);
|
|
|
|
for (WordListMetadata wlData : to) wordListIds.add(wlData.mId);
|
|
|
|
for (String id : wordListIds) {
|
|
|
|
final WordListMetadata currentInfo = MetadataHandler.findWordListById(from, id);
|
|
|
|
final WordListMetadata metadataInfo = MetadataHandler.findWordListById(to, 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;
|
2013-06-25 08:03:05 +00:00
|
|
|
DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo);
|
2013-03-15 10:00:51 +00:00
|
|
|
|
|
|
|
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"
|
2013-07-10 06:27:26 +00:00
|
|
|
+ " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
|
2013-03-15 10:00:51 +00:00
|
|
|
}
|
|
|
|
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 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, false));
|
|
|
|
} 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 Locale locale = LocaleUtils.constructLocaleFromString(localeString);
|
|
|
|
final String language = (null == locale ? "" : locale.getDisplayLanguage());
|
|
|
|
final String titleFormat = context.getString(R.string.dict_available_notification_title);
|
|
|
|
final String notificationTitle = String.format(titleFormat, language);
|
2014-08-28 04:03:32 +00:00
|
|
|
final Notification.Builder builder = new Notification.Builder(context)
|
2013-03-15 10:00:51 +00:00
|
|
|
.setAutoCancel(true)
|
|
|
|
.setContentIntent(notificationIntent)
|
|
|
|
.setContentTitle(notificationTitle)
|
|
|
|
.setContentText(context.getString(R.string.dict_available_notification_description))
|
|
|
|
.setTicker(notificationTitle)
|
|
|
|
.setOngoing(false)
|
|
|
|
.setOnlyAlertOnce(true)
|
2014-08-28 04:03:32 +00:00
|
|
|
.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);
|
2013-03-15 10:00:51 +00:00
|
|
|
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, final boolean mayPrompt) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mayPrompt
|
|
|
|
&& DOWNLOAD_OVER_METERED_SETTING_UNKNOWN
|
|
|
|
== getDownloadOverMeteredSetting(context)) {
|
|
|
|
final ConnectivityManager cm =
|
|
|
|
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
|
|
if (ConnectivityManagerCompatUtils.isActiveNetworkMetered(cm)) {
|
|
|
|
showDictionaryAvailableNotification(context, clientId, installCandidate);
|
|
|
|
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();
|
|
|
|
actions.add(new ActionBatch.StartDownloadAction(clientId,
|
|
|
|
WordListMetadata.createFromContentValues(installCandidate), false));
|
|
|
|
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: we know that from the mayPrompt argument.
|
|
|
|
if (mayPrompt) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
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 List<WordListMetadata> currentMetadata =
|
|
|
|
MetadataHandler.getCurrentMetadata(context, clientId);
|
|
|
|
WordListMetadata wordList = MetadataHandler.findWordListById(currentMetadata, wordlistId);
|
|
|
|
if (null == wordList) return;
|
|
|
|
final ActionBatch actions = new ActionBatch();
|
|
|
|
if (MetadataDbHelper.STATUS_DISABLED == status
|
|
|
|
|| MetadataDbHelper.STATUS_DELETING == status) {
|
|
|
|
actions.add(new ActionBatch.EnableAction(clientId, wordList));
|
|
|
|
} else if (MetadataDbHelper.STATUS_AVAILABLE == status) {
|
|
|
|
actions.add(new ActionBatch.StartDownloadAction(clientId, wordList,
|
|
|
|
allowDownloadOnMeteredData));
|
|
|
|
} 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 List<WordListMetadata> currentMetadata =
|
|
|
|
MetadataHandler.getCurrentMetadata(context, clientId);
|
|
|
|
final WordListMetadata wordList =
|
|
|
|
MetadataHandler.findWordListById(currentMetadata, wordlistId);
|
|
|
|
if (null == wordList) return;
|
|
|
|
final ActionBatch actions = new ActionBatch();
|
|
|
|
actions.add(new ActionBatch.DisableAction(clientId, wordList));
|
|
|
|
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 List<WordListMetadata> currentMetadata =
|
|
|
|
MetadataHandler.getCurrentMetadata(context, clientId);
|
|
|
|
final WordListMetadata wordList =
|
|
|
|
MetadataHandler.findWordListById(currentMetadata, wordlistId);
|
|
|
|
if (null == wordList) return;
|
|
|
|
final ActionBatch actions = new ActionBatch();
|
|
|
|
actions.add(new ActionBatch.DisableAction(clientId, wordList));
|
|
|
|
actions.add(new ActionBatch.StartDeleteAction(clientId, wordList));
|
|
|
|
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 List<WordListMetadata> currentMetadata =
|
|
|
|
MetadataHandler.getCurrentMetadata(context, clientId);
|
|
|
|
final WordListMetadata wordList =
|
|
|
|
MetadataHandler.findWordListById(currentMetadata, wordlistId);
|
|
|
|
if (null == wordList) return;
|
|
|
|
final ActionBatch actions = new ActionBatch();
|
|
|
|
actions.add(new ActionBatch.FinishDeleteAction(clientId, wordList));
|
|
|
|
actions.execute(context, new LogProblemReporter(TAG));
|
|
|
|
signalNewDictionaryState(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Marks the word list with the passed id as broken.
|
|
|
|
*
|
|
|
|
* This effectively deletes the entry from the metadata. It doesn't prevent the same
|
|
|
|
* word list to be downloaded again at a later time if the same or a new version is
|
|
|
|
* available the next time we download the metadata.
|
|
|
|
*
|
|
|
|
* @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 broken.
|
|
|
|
* @param version the version of the word list to mark as deleted.
|
|
|
|
*/
|
|
|
|
public static void markAsBroken(final Context context, final String clientId,
|
|
|
|
final String wordlistId, final int version) {
|
|
|
|
// TODO: do this on another thread to avoid blocking the UI.
|
|
|
|
MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId),
|
|
|
|
wordlistId, version);
|
|
|
|
}
|
|
|
|
}
|