diff options
Diffstat (limited to 'src/com')
16 files changed, 1029 insertions, 1413 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java index 8d806182..08ef4665 100644 --- a/src/com/android/providers/downloads/Constants.java +++ b/src/com/android/providers/downloads/Constants.java @@ -48,9 +48,6 @@ public class Constants { /** The column that is used to remember whether the media scanner was invoked */ public static final String MEDIA_SCANNED = "scanned"; - /** The column that is used to count retries */ - public static final String FAILED_CONNECTIONS = "numfailed"; - /** The intent that gets sent when the service must wake up for a retry */ public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; diff --git a/src/com/android/providers/downloads/DownloadDrmHelper.java b/src/com/android/providers/downloads/DownloadDrmHelper.java index 10cb792c..d1358246 100644 --- a/src/com/android/providers/downloads/DownloadDrmHelper.java +++ b/src/com/android/providers/downloads/DownloadDrmHelper.java @@ -19,7 +19,8 @@ package com.android.providers.downloads; import android.content.Context; import android.drm.DrmManagerClient; -import android.util.Log; + +import java.io.File; public class DownloadDrmHelper { @@ -32,31 +33,6 @@ public class DownloadDrmHelper { public static final String EXTENSION_INTERNAL_FWDL = ".fl"; /** - * Checks if the Media Type is a DRM Media Type - * - * @param drmManagerClient A DrmManagerClient - * @param mimetype Media Type to check - * @return True if the Media Type is DRM else false - */ - public static boolean isDrmMimeType(Context context, String mimetype) { - boolean result = false; - if (context != null) { - try { - DrmManagerClient drmClient = new DrmManagerClient(context); - if (drmClient != null && mimetype != null && mimetype.length() > 0) { - result = drmClient.canHandle("", mimetype); - } - } catch (IllegalArgumentException e) { - Log.w(Constants.TAG, - "DrmManagerClient instance could not be created, context is Illegal."); - } catch (IllegalStateException e) { - Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly."); - } - } - return result; - } - - /** * Checks if the Media Type needs to be DRM converted * * @param mimetype Media type of the content @@ -83,28 +59,20 @@ public class DownloadDrmHelper { } /** - * Gets the original mime type of DRM protected content. - * - * @param context The context - * @param path Path to the file - * @param containingMime The current mime type of of the file i.e. the - * containing mime type - * @return The original mime type of the file if DRM protected else the - * currentMime + * Return the original MIME type of the given file, using the DRM framework + * if the file is protected content. */ - public static String getOriginalMimeType(Context context, String path, String containingMime) { - String result = containingMime; - DrmManagerClient drmClient = new DrmManagerClient(context); + public static String getOriginalMimeType(Context context, File file, String currentMime) { + final DrmManagerClient client = new DrmManagerClient(context); try { - if (drmClient.canHandle(path, null)) { - result = drmClient.getOriginalMimeType(path); + final String rawFile = file.toString(); + if (client.canHandle(rawFile, null)) { + return client.getOriginalMimeType(rawFile); + } else { + return currentMime; } - } catch (IllegalArgumentException ex) { - Log.w(Constants.TAG, - "Can't get original mime type since path is null or empty string."); - } catch (IllegalStateException ex) { - Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly."); + } finally { + client.release(); } - return result; } } diff --git a/src/com/android/providers/downloads/DownloadHandler.java b/src/com/android/providers/downloads/DownloadHandler.java deleted file mode 100644 index 2f02864e..00000000 --- a/src/com/android/providers/downloads/DownloadHandler.java +++ /dev/null @@ -1,123 +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.providers.downloads; - -import android.content.res.Resources; -import android.util.Log; -import android.util.LongSparseArray; - -import com.android.internal.annotations.GuardedBy; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; - -public class DownloadHandler { - private static final String TAG = "DownloadHandler"; - - @GuardedBy("this") - private final LinkedHashMap<Long, DownloadInfo> mDownloadsQueue = - new LinkedHashMap<Long, DownloadInfo>(); - @GuardedBy("this") - private final HashMap<Long, DownloadInfo> mDownloadsInProgress = - new HashMap<Long, DownloadInfo>(); - @GuardedBy("this") - private final LongSparseArray<Long> mCurrentSpeed = new LongSparseArray<Long>(); - - private final int mMaxConcurrentDownloadsAllowed = Resources.getSystem().getInteger( - com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed); - - private static final DownloadHandler sDownloadHandler = new DownloadHandler(); - - public static DownloadHandler getInstance() { - return sDownloadHandler; - } - - public synchronized void enqueueDownload(DownloadInfo info) { - if (!mDownloadsQueue.containsKey(info.mId)) { - if (Constants.LOGV) { - Log.i(TAG, "enqueued download. id: " + info.mId + ", uri: " + info.mUri); - } - mDownloadsQueue.put(info.mId, info); - startDownloadThreadLocked(); - } - } - - private void startDownloadThreadLocked() { - Iterator<Long> keys = mDownloadsQueue.keySet().iterator(); - ArrayList<Long> ids = new ArrayList<Long>(); - while (mDownloadsInProgress.size() < mMaxConcurrentDownloadsAllowed && keys.hasNext()) { - Long id = keys.next(); - DownloadInfo info = mDownloadsQueue.get(id); - info.startDownloadThread(); - ids.add(id); - mDownloadsInProgress.put(id, mDownloadsQueue.get(id)); - if (Constants.LOGV) { - Log.i(TAG, "started download for : " + id); - } - } - for (Long id : ids) { - mDownloadsQueue.remove(id); - } - } - - public synchronized boolean hasDownloadInQueue(long id) { - return mDownloadsQueue.containsKey(id) || mDownloadsInProgress.containsKey(id); - } - - public synchronized void dequeueDownload(long id) { - mDownloadsInProgress.remove(id); - mCurrentSpeed.remove(id); - startDownloadThreadLocked(); - if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) { - notifyAll(); - } - } - - public synchronized void setCurrentSpeed(long id, long speed) { - mCurrentSpeed.put(id, speed); - } - - public synchronized long getCurrentSpeed(long id) { - return mCurrentSpeed.get(id, -1L); - } - - // right now this is only used by tests. but there is no reason why it can't be used - // by any module using DownloadManager (TODO add API to DownloadManager.java) - public synchronized void waitUntilDownloadsTerminate() throws InterruptedException { - if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) { - if (Constants.LOGVV) { - Log.i(TAG, "nothing to wait on"); - } - return; - } - if (Constants.LOGVV) { - for (DownloadInfo info : mDownloadsInProgress.values()) { - Log.i(TAG, "** progress: " + info.mId + ", " + info.mUri); - } - for (DownloadInfo info : mDownloadsQueue.values()) { - Log.i(TAG, "** in Q: " + info.mId + ", " + info.mUri); - } - } - if (Constants.LOGVV) { - Log.i(TAG, "waiting for 5 sec"); - } - // wait upto 5 sec - wait(5 * 1000); - } -} diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java index 5172b696..e6ed059b 100644 --- a/src/com/android/providers/downloads/DownloadInfo.java +++ b/src/com/android/providers/downloads/DownloadInfo.java @@ -31,20 +31,26 @@ import android.os.Environment; import android.provider.Downloads; import android.provider.Downloads.Impl; import android.text.TextUtils; -import android.util.Log; import android.util.Pair; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; /** * Stores information about an individual download. */ public class DownloadInfo { + // TODO: move towards these in-memory objects being sources of truth, and + // periodically pushing to provider. + public static class Reader { private ContentResolver mResolver; private Cursor mCursor; @@ -54,8 +60,10 @@ public class DownloadInfo { mCursor = cursor; } - public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade) { - DownloadInfo info = new DownloadInfo(context, systemFacade); + public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade, + StorageManager storageManager, DownloadNotifier notifier) { + final DownloadInfo info = new DownloadInfo( + context, systemFacade, storageManager, notifier); updateFromDatabase(info); readRequestHeaders(info); return info; @@ -71,7 +79,7 @@ public class DownloadInfo { info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION); info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY); info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS); - info.mNumFailed = getInt(Constants.FAILED_CONNECTIONS); + info.mNumFailed = getInt(Downloads.Impl.COLUMN_FAILED_CONNECTIONS); int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT); info.mRetryAfter = retryRedirect & 0xfffffff; info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION); @@ -146,44 +154,49 @@ public class DownloadInfo { } } - // the following NETWORK_* constants are used to indicates specfic reasons for disallowing a - // download from using a network, since specific causes can require special handling - - /** - * The network is usable for the given download. - */ - public static final int NETWORK_OK = 1; - - /** - * There is no network connectivity. - */ - public static final int NETWORK_NO_CONNECTION = 2; - - /** - * The download exceeds the maximum size for this network. - */ - public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3; - - /** - * The download exceeds the recommended maximum size for this network, the user must confirm for - * this download to proceed without WiFi. - */ - public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4; - /** - * The current connection is roaming, and the download can't proceed over a roaming connection. + * Constants used to indicate network state for a specific download, after + * applying any requested constraints. */ - public static final int NETWORK_CANNOT_USE_ROAMING = 5; - - /** - * The app requesting the download specific that it can't use the current network connection. - */ - public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6; - - /** - * Current network is blocked for requesting application. - */ - public static final int NETWORK_BLOCKED = 7; + public enum NetworkState { + /** + * The network is usable for the given download. + */ + OK, + + /** + * There is no network connectivity. + */ + NO_CONNECTION, + + /** + * The download exceeds the maximum size for this network. + */ + UNUSABLE_DUE_TO_SIZE, + + /** + * The download exceeds the recommended maximum size for this network, + * the user must confirm for this download to proceed without WiFi. + */ + RECOMMENDED_UNUSABLE_DUE_TO_SIZE, + + /** + * The current connection is roaming, and the download can't proceed + * over a roaming connection. + */ + CANNOT_USE_ROAMING, + + /** + * The app requesting the download specific that it can't use the + * current network connection. + */ + TYPE_DISALLOWED_BY_REQUESTOR, + + /** + * Current network is blocked for requesting application. + */ + BLOCKED; + } /** * For intents used to notify the user that a download exceeds a size threshold, if this extra @@ -191,7 +204,6 @@ public class DownloadInfo { */ public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; - public long mId; public String mUri; public boolean mNoIntegrity; @@ -229,12 +241,25 @@ public class DownloadInfo { public int mFuzz; private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>(); - private SystemFacade mSystemFacade; - private Context mContext; - private DownloadInfo(Context context, SystemFacade systemFacade) { + /** + * Result of last {@link DownloadThread} started by + * {@link #startDownloadIfReady(ExecutorService)}. + */ + @GuardedBy("this") + private Future<?> mActiveDownload; + + private final Context mContext; + private final SystemFacade mSystemFacade; + private final StorageManager mStorageManager; + private final DownloadNotifier mNotifier; + + private DownloadInfo(Context context, SystemFacade systemFacade, StorageManager storageManager, + DownloadNotifier notifier) { mContext = context; mSystemFacade = systemFacade; + mStorageManager = storageManager; + mNotifier = notifier; mFuzz = Helpers.sRandom.nextInt(1001); } @@ -285,14 +310,9 @@ public class DownloadInfo { } /** - * Returns whether this download (which the download manager hasn't seen yet) - * should be started. + * Returns whether this download should be enqueued. */ - private boolean isReadyToStart(long now) { - if (DownloadHandler.getInstance().hasDownloadInQueue(mId)) { - // already running - return false; - } + private boolean isReadyToDownload() { if (mControl == Downloads.Impl.CONTROL_PAUSED) { // the download is paused, so it's not going to start return false; @@ -306,10 +326,11 @@ public class DownloadInfo { case Downloads.Impl.STATUS_WAITING_FOR_NETWORK: case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: - return checkCanUseNetwork() == NETWORK_OK; + return checkCanUseNetwork() == NetworkState.OK; case Downloads.Impl.STATUS_WAITING_TO_RETRY: // download was waiting for a delayed restart + final long now = mSystemFacade.currentTimeMillis(); return restartTime(now) <= now; case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR: // is the media mounted? @@ -337,21 +358,20 @@ public class DownloadInfo { /** * Returns whether this download is allowed to use the network. - * @return one of the NETWORK_* constants */ - public int checkCanUseNetwork() { + public NetworkState checkCanUseNetwork() { final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid); if (info == null || !info.isConnected()) { - return NETWORK_NO_CONNECTION; + return NetworkState.NO_CONNECTION; } if (DetailedState.BLOCKED.equals(info.getDetailedState())) { - return NETWORK_BLOCKED; + return NetworkState.BLOCKED; } - if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) { - return NETWORK_CANNOT_USE_ROAMING; + if (mSystemFacade.isNetworkRoaming() && !isRoamingAllowed()) { + return NetworkState.CANNOT_USE_ROAMING; } - if (!mAllowMetered && mSystemFacade.isActiveNetworkMetered()) { - return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; + if (mSystemFacade.isActiveNetworkMetered() && !mAllowMetered) { + return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; } return checkIsNetworkTypeAllowed(info.getType()); } @@ -365,45 +385,16 @@ public class DownloadInfo { } /** - * @return a non-localized string appropriate for logging corresponding to one of the - * NETWORK_* constants. - */ - public String getLogMessageForNetworkError(int networkError) { - switch (networkError) { - case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: - return "download size exceeds recommended limit for mobile network"; - - case NETWORK_UNUSABLE_DUE_TO_SIZE: - return "download size exceeds limit for mobile network"; - - case NETWORK_NO_CONNECTION: - return "no network connection available"; - - case NETWORK_CANNOT_USE_ROAMING: - return "download cannot use the current network connection because it is roaming"; - - case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR: - return "download was requested to not use the current network type"; - - case NETWORK_BLOCKED: - return "network is blocked for requesting application"; - - default: - return "unknown error with network connectivity"; - } - } - - /** * Check if this download can proceed over the given network type. * @param networkType a constant from ConnectivityManager.TYPE_*. * @return one of the NETWORK_* constants */ - private int checkIsNetworkTypeAllowed(int networkType) { + private NetworkState checkIsNetworkTypeAllowed(int networkType) { if (mIsPublicApi) { final int flag = translateNetworkTypeToApiFlag(networkType); final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0; if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) { - return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR; + return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; } } return checkSizeAllowedForNetwork(networkType); @@ -433,42 +424,68 @@ public class DownloadInfo { * Check if the download's size prohibits it from running over the current network. * @return one of the NETWORK_* constants */ - private int checkSizeAllowedForNetwork(int networkType) { + private NetworkState checkSizeAllowedForNetwork(int networkType) { if (mTotalBytes <= 0) { - return NETWORK_OK; // we don't know the size yet + return NetworkState.OK; // we don't know the size yet } if (networkType == ConnectivityManager.TYPE_WIFI) { - return NETWORK_OK; // anything goes over wifi + return NetworkState.OK; // anything goes over wifi } Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile(); if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) { - return NETWORK_UNUSABLE_DUE_TO_SIZE; + return NetworkState.UNUSABLE_DUE_TO_SIZE; } if (mBypassRecommendedSizeLimit == 0) { Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile(); if (recommendedMaxBytesOverMobile != null && mTotalBytes > recommendedMaxBytesOverMobile) { - return NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE; + return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE; } } - return NETWORK_OK; + return NetworkState.OK; } - void startIfReady(long now, StorageManager storageManager) { - if (!isReadyToStart(now)) { - return; - } + /** + * If download is ready to start, and isn't already pending or executing, + * create a {@link DownloadThread} and enqueue it into given + * {@link Executor}. + * + * @return If actively downloading. + */ + public boolean startDownloadIfReady(ExecutorService executor) { + synchronized (this) { + final boolean isReady = isReadyToDownload(); + final boolean isActive = mActiveDownload != null && !mActiveDownload.isDone(); + if (isReady && !isActive) { + if (mStatus != Impl.STATUS_RUNNING) { + mStatus = Impl.STATUS_RUNNING; + ContentValues values = new ContentValues(); + values.put(Impl.COLUMN_STATUS, mStatus); + mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null); + } - if (Constants.LOGV) { - Log.v(Constants.TAG, "Service spawning thread to handle download " + mId); + final DownloadThread task = new DownloadThread( + mContext, mSystemFacade, this, mStorageManager, mNotifier); + mActiveDownload = executor.submit(task); + } + return isReady; } - if (mStatus != Impl.STATUS_RUNNING) { - mStatus = Impl.STATUS_RUNNING; - ContentValues values = new ContentValues(); - values.put(Impl.COLUMN_STATUS, mStatus); - mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null); + } + + /** + * If download is ready to be scanned, enqueue it into the given + * {@link DownloadScanner}. + * + * @return If actively scanning. + */ + public boolean startScanIfReady(DownloadScanner scanner) { + synchronized (this) { + final boolean isReady = shouldScanFile(); + if (isReady) { + scanner.requestScan(this); + } + return isReady; } - DownloadHandler.getInstance().enqueueDownload(this); } public boolean isOnCache() { @@ -529,15 +546,15 @@ public class DownloadInfo { } /** - * Returns the amount of time (as measured from the "now" parameter) - * at which a download will be active. - * 0 = immediately - service should stick around to handle this download. - * -1 = never - service can go away without ever waking up. - * positive value - service must wake up in the future, as specified in ms from "now" + * Return time when this download will be ready for its next action, in + * milliseconds after given time. + * + * @return If {@code 0}, download is ready to proceed immediately. If + * {@link Long#MAX_VALUE}, then download has no future actions. */ - long nextAction(long now) { + public long nextActionMillis(long now) { if (Downloads.Impl.isStatusCompleted(mStatus)) { - return -1; + return Long.MAX_VALUE; } if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) { return 0; @@ -552,7 +569,7 @@ public class DownloadInfo { /** * Returns whether a file should be scanned */ - boolean shouldScanFile() { + public boolean shouldScanFile() { return (mMediaScanned == 0) && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL || mDestination == Downloads.Impl.DESTINATION_FILE_URI || @@ -570,12 +587,6 @@ public class DownloadInfo { mContext.startActivity(intent); } - void startDownloadThread() { - DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this, - StorageManager.getInstance(mContext)); - mSystemFacade.startThread(downloader); - } - /** * Query and return status of requested download. */ diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java index daae7831..0af9cb86 100644 --- a/src/com/android/providers/downloads/DownloadNotifier.java +++ b/src/com/android/providers/downloads/DownloadNotifier.java @@ -19,6 +19,7 @@ package com.android.providers.downloads; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION; +import static android.provider.Downloads.Impl.STATUS_RUNNING; import android.app.DownloadManager; import android.app.Notification; @@ -32,6 +33,7 @@ import android.net.Uri; import android.provider.Downloads; import android.text.TextUtils; import android.text.format.DateUtils; +import android.util.LongSparseLongArray; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Maps; @@ -66,6 +68,13 @@ public class DownloadNotifier { @GuardedBy("mActiveNotifs") private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap(); + /** + * Current speed of active downloads, mapped from {@link DownloadInfo#mId} + * to speed in bytes per second. + */ + @GuardedBy("mDownloadSpeed") + private final LongSparseLongArray mDownloadSpeed = new LongSparseLongArray(); + public DownloadNotifier(Context context) { mContext = context; mNotifManager = (NotificationManager) context.getSystemService( @@ -77,6 +86,20 @@ public class DownloadNotifier { } /** + * Notify the current speed of an active download, used for calcuating + * estimated remaining time. + */ + public void notifyDownloadSpeed(long id, long bytesPerSecond) { + synchronized (mDownloadSpeed) { + if (bytesPerSecond != 0) { + mDownloadSpeed.put(id, bytesPerSecond); + } else { + mDownloadSpeed.delete(id); + } + } + } + + /** * Update {@link NotificationManager} to reflect the given set of * {@link DownloadInfo}, adding, collapsing, and removing as needed. */ @@ -163,16 +186,16 @@ public class DownloadNotifier { String remainingText = null; String percentText = null; if (type == TYPE_ACTIVE) { - final DownloadHandler handler = DownloadHandler.getInstance(); - long current = 0; long total = 0; long speed = 0; - for (DownloadInfo info : cluster) { - if (info.mTotalBytes != -1) { - current += info.mCurrentBytes; - total += info.mTotalBytes; - speed += handler.getCurrentSpeed(info.mId); + synchronized (mDownloadSpeed) { + for (DownloadInfo info : cluster) { + if (info.mTotalBytes != -1) { + current += info.mCurrentBytes; + total += info.mTotalBytes; + speed += mDownloadSpeed.get(info.mId); + } } } @@ -305,7 +328,7 @@ public class DownloadNotifier { } private static boolean isActiveAndVisible(DownloadInfo download) { - return Downloads.Impl.isStatusInformational(download.mStatus) && + return download.mStatus == STATUS_RUNNING && (download.mVisibility == VISIBILITY_VISIBLE || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED); } diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index c554e41d..2d0c807a 100644 --- a/src/com/android/providers/downloads/DownloadProvider.java +++ b/src/com/android/providers/downloads/DownloadProvider.java @@ -37,17 +37,22 @@ import android.os.Binder; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.Process; +import android.provider.BaseColumns; import android.provider.Downloads; import android.provider.OpenableColumns; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Log; +import com.android.internal.util.IndentingPrintWriter; import com.google.android.collect.Maps; import com.google.common.annotations.VisibleForTesting; import java.io.File; +import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -384,7 +389,7 @@ public final class DownloadProvider extends ContentProvider { Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " + Downloads.Impl.COLUMN_CONTROL + " INTEGER, " + Downloads.Impl.COLUMN_STATUS + " INTEGER, " + - Constants.FAILED_CONNECTIONS + " INTEGER, " + + Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " + Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " + Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " + Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " + @@ -446,7 +451,7 @@ public final class DownloadProvider extends ContentProvider { // saves us by getting some initialization code in DownloadService out of the way. Context context = getContext(); context.startService(new Intent(context, DownloadService.class)); - mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory(); + mDownloadsDataDir = StorageManager.getDownloadDataDirectory(getContext()); return true; } @@ -1199,6 +1204,41 @@ public final class DownloadProvider extends ContentProvider { return ret; } + @Override + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120); + + pw.println("Downloads updated in last hour:"); + pw.increaseIndent(); + + final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS; + final Cursor cursor = db.query(DB_TABLE, null, + Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null, + Downloads.Impl._ID + " ASC"); + try { + final String[] cols = cursor.getColumnNames(); + final int idCol = cursor.getColumnIndex(BaseColumns._ID); + while (cursor.moveToNext()) { + pw.println("Download #" + cursor.getInt(idCol) + ":"); + pw.increaseIndent(); + for (int i = 0; i < cols.length; i++) { + // Omit sensitive data when dumping + if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) { + continue; + } + pw.printPair(cols[i], cursor.getString(i)); + } + pw.println(); + pw.decreaseIndent(); + } + } finally { + cursor.close(); + } + + pw.decreaseIndent(); + } + private void logVerboseOpenFileInfo(Uri uri, String mode) { Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode + ", uid: " + Binder.getCallingUid()); @@ -1229,7 +1269,7 @@ public final class DownloadProvider extends ContentProvider { Log.v(Constants.TAG, "file exists in openFile"); } } - cursor.close(); + cursor.close(); } } diff --git a/src/com/android/providers/downloads/DownloadScanner.java b/src/com/android/providers/downloads/DownloadScanner.java new file mode 100644 index 00000000..ca795062 --- /dev/null +++ b/src/com/android/providers/downloads/DownloadScanner.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2013 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.providers.downloads; + +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static com.android.providers.downloads.Constants.LOGV; +import static com.android.providers.downloads.Constants.TAG; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.net.Uri; +import android.os.SystemClock; +import android.provider.Downloads; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.google.common.collect.Maps; + +import java.util.HashMap; + +/** + * Manages asynchronous scanning of completed downloads. + */ +public class DownloadScanner implements MediaScannerConnectionClient { + private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS; + + private final Context mContext; + private final MediaScannerConnection mConnection; + + private static class ScanRequest { + public final long id; + public final String path; + public final String mimeType; + public final long requestRealtime; + + public ScanRequest(long id, String path, String mimeType) { + this.id = id; + this.path = path; + this.mimeType = mimeType; + this.requestRealtime = SystemClock.elapsedRealtime(); + } + + public void exec(MediaScannerConnection conn) { + conn.scanFile(path, mimeType); + } + } + + @GuardedBy("mConnection") + private HashMap<String, ScanRequest> mPending = Maps.newHashMap(); + + public DownloadScanner(Context context) { + mContext = context; + mConnection = new MediaScannerConnection(context, this); + } + + /** + * Check if requested scans are still pending. Scans may timeout after an + * internal duration. + */ + public boolean hasPendingScans() { + synchronized (mConnection) { + if (mPending.isEmpty()) { + return false; + } else { + // Check if pending scans have timed out + final long nowRealtime = SystemClock.elapsedRealtime(); + for (ScanRequest req : mPending.values()) { + if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) { + return true; + } + } + return false; + } + } + } + + /** + * Request that given {@link DownloadInfo} be scanned at some point in + * future. Enqueues the request to be scanned asynchronously. + * + * @see #hasPendingScans() + */ + public void requestScan(DownloadInfo info) { + if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName); + synchronized (mConnection) { + final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType); + mPending.put(req.path, req); + + if (mConnection.isConnected()) { + req.exec(mConnection); + } else { + mConnection.connect(); + } + } + } + + public void shutdown() { + mConnection.disconnect(); + } + + @Override + public void onMediaScannerConnected() { + synchronized (mConnection) { + for (ScanRequest req : mPending.values()) { + req.exec(mConnection); + } + } + } + + @Override + public void onScanCompleted(String path, Uri uri) { + final ScanRequest req; + synchronized (mConnection) { + req = mPending.remove(path); + } + if (req == null) { + Log.w(TAG, "Missing request for path " + path); + return; + } + + // Update scanned column, which will kick off a database update pass, + // eventually deciding if overall service is ready for teardown. + final ContentValues values = new ContentValues(); + values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1); + if (uri != null) { + values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString()); + } + + final ContentResolver resolver = mContext.getContentResolver(); + final Uri downloadUri = ContentUris.withAppendedId( + Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id); + final int rows = resolver.update(downloadUri, values, null, null); + if (rows == 0) { + // Local row disappeared during scan; download was probably deleted + // so clean up now-orphaned media entry. + resolver.delete(uri, null, null); + } + } +} diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java index b97346b2..d6ed9d6d 100644 --- a/src/com/android/providers/downloads/DownloadService.java +++ b/src/com/android/providers/downloads/DownloadService.java @@ -16,25 +16,25 @@ package com.android.providers.downloads; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.providers.downloads.Constants.TAG; import android.app.AlarmManager; +import android.app.DownloadManager; import android.app.PendingIntent; import android.app.Service; -import android.content.ComponentName; -import android.content.ContentValues; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.ServiceConnection; +import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; -import android.media.IMediaScannerListener; -import android.media.IMediaScannerService; import android.net.Uri; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.Message; import android.os.Process; -import android.os.RemoteException; import android.provider.Downloads; import android.text.TextUtils; import android.util.Log; @@ -44,22 +44,40 @@ import com.android.internal.util.IndentingPrintWriter; import com.google.android.collect.Maps; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** - * Performs the background downloads requested by applications that use the Downloads provider. + * Performs background downloads as requested by applications that use + * {@link DownloadManager}. Multiple start commands can be issued at this + * service, and it will continue running until no downloads are being actively + * processed. It may schedule alarms to resume downloads in future. + * <p> + * Any database updates important enough to initiate tasks should always be + * delivered through {@link Context#startService(Intent)}. */ public class DownloadService extends Service { - /** amount of time to wait to connect to MediaScannerService before timing out */ - private static final long WAIT_TIMEOUT = 10 * 1000; + // TODO: migrate WakeLock from individual DownloadThreads out into + // DownloadReceiver to protect our entire workflow. + + private static final boolean DEBUG_LIFECYCLE = true; + + @VisibleForTesting + SystemFacade mSystemFacade; + + private AlarmManager mAlarmManager; + private StorageManager mStorageManager; /** Observer to get notified when the content observer's data changes */ private DownloadManagerContentObserver mObserver; @@ -74,118 +92,41 @@ public class DownloadService extends Service { * content provider changes or disappears. */ @GuardedBy("mDownloads") - private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); + private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); - /** - * The thread that updates the internal download list from the content - * provider. - */ - @VisibleForTesting - UpdateThread mUpdateThread; + private final ExecutorService mExecutor = buildDownloadExecutor(); - /** - * Whether the internal download list should be updated from the content - * provider. - */ - private boolean mPendingUpdate; + private static ExecutorService buildDownloadExecutor() { + final int maxConcurrent = Resources.getSystem().getInteger( + com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed); - /** - * The ServiceConnection object that tells us when we're connected to and disconnected from - * the Media Scanner - */ - private MediaScannerConnection mMediaScannerConnection; + // Create a bounded thread pool for executing downloads; it creates + // threads as needed (up to maximum) and reclaims them when finished. + final ThreadPoolExecutor executor = new ThreadPoolExecutor( + maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>()); + executor.allowCoreThreadTimeOut(true); + return executor; + } - private boolean mMediaScannerConnecting; + private DownloadScanner mScanner; - /** - * The IPC interface to the Media Scanner - */ - private IMediaScannerService mMediaScannerService; + private HandlerThread mUpdateThread; + private Handler mUpdateHandler; - @VisibleForTesting - SystemFacade mSystemFacade; - - private StorageManager mStorageManager; + private volatile int mLastStartId; /** * Receives notifications when the data in the content provider changes */ private class DownloadManagerContentObserver extends ContentObserver { - public DownloadManagerContentObserver() { super(new Handler()); } - /** - * Receives notification when the data in the observed content - * provider changes. - */ @Override public void onChange(final boolean selfChange) { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Service ContentObserver received notification"); - } - updateFromProvider(); - } - - } - - /** - * Gets called back when the connection to the media - * scanner is established or lost. - */ - public class MediaScannerConnection implements ServiceConnection { - public void onServiceConnected(ComponentName className, IBinder service) { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Connected to Media Scanner"); - } - synchronized (DownloadService.this) { - try { - mMediaScannerConnecting = false; - mMediaScannerService = IMediaScannerService.Stub.asInterface(service); - if (mMediaScannerService != null) { - updateFromProvider(); - } - } finally { - // notify anyone waiting on successful connection to MediaService - DownloadService.this.notifyAll(); - } - } - } - - public void disconnectMediaScanner() { - synchronized (DownloadService.this) { - mMediaScannerConnecting = false; - if (mMediaScannerService != null) { - mMediaScannerService = null; - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Disconnecting from Media Scanner"); - } - try { - unbindService(this); - } catch (IllegalArgumentException ex) { - Log.w(Constants.TAG, "unbindService failed: " + ex); - } finally { - // notify anyone waiting on unsuccessful connection to MediaService - DownloadService.this.notifyAll(); - } - } - } - } - - public void onServiceDisconnected(ComponentName className) { - try { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Disconnected from Media Scanner"); - } - } finally { - synchronized (DownloadService.this) { - mMediaScannerService = null; - mMediaScannerConnecting = false; - // notify anyone waiting on disconnect from MediaService - DownloadService.this.notifyAll(); - } - } + enqueueUpdate(); } } @@ -214,19 +155,21 @@ public class DownloadService extends Service { mSystemFacade = new RealSystemFacade(this); } - mObserver = new DownloadManagerContentObserver(); - getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, - true, mObserver); + mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + mStorageManager = new StorageManager(this); + + mUpdateThread = new HandlerThread(TAG + "-UpdateThread"); + mUpdateThread.start(); + mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback); - mMediaScannerService = null; - mMediaScannerConnecting = false; - mMediaScannerConnection = new MediaScannerConnection(); + mScanner = new DownloadScanner(this); mNotifier = new DownloadNotifier(this); mNotifier.cancelAll(); - mStorageManager = StorageManager.getInstance(getApplicationContext()); - updateFromProvider(); + mObserver = new DownloadManagerContentObserver(); + getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, + true, mObserver); } @Override @@ -235,16 +178,16 @@ public class DownloadService extends Service { if (Constants.LOGVV) { Log.v(Constants.TAG, "Service onStart"); } - updateFromProvider(); + mLastStartId = startId; + enqueueUpdate(); return returnValue; } - /** - * Cleans up when the service is destroyed - */ @Override public void onDestroy() { getContentResolver().unregisterContentObserver(mObserver); + mScanner.shutdown(); + mUpdateThread.quit(); if (Constants.LOGVV) { Log.v(Constants.TAG, "Service onDestroy"); } @@ -252,182 +195,166 @@ public class DownloadService extends Service { } /** - * Parses data from the content provider into private array + * Enqueue an {@link #updateLocked()} pass to occur in future. */ - private void updateFromProvider() { - synchronized (this) { - mPendingUpdate = true; - if (mUpdateThread == null) { - mUpdateThread = new UpdateThread(); - mSystemFacade.startThread(mUpdateThread); - } - } + private void enqueueUpdate() { + mUpdateHandler.removeMessages(MSG_UPDATE); + mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget(); } - private class UpdateThread extends Thread { - public UpdateThread() { - super("Download Service"); - } + /** + * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to + * catch any finished operations that didn't trigger an update pass. + */ + private void enqueueFinalUpdate() { + mUpdateHandler.removeMessages(MSG_FINAL_UPDATE); + mUpdateHandler.sendMessageDelayed( + mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1), + 90 * SECOND_IN_MILLIS); + } + + private static final int MSG_UPDATE = 1; + private static final int MSG_FINAL_UPDATE = 2; + private Handler.Callback mUpdateCallback = new Handler.Callback() { @Override - public void run() { + public boolean handleMessage(Message msg) { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - boolean keepService = false; - // for each update from the database, remember which download is - // supposed to get restarted soonest in the future - long wakeUp = Long.MAX_VALUE; - for (;;) { - synchronized (DownloadService.this) { - if (mUpdateThread != this) { - throw new IllegalStateException( - "multiple UpdateThreads in DownloadService"); - } - if (!mPendingUpdate) { - mUpdateThread = null; - if (!keepService) { - stopSelf(); - } - if (wakeUp != Long.MAX_VALUE) { - scheduleAlarm(wakeUp); - } - return; - } - mPendingUpdate = false; + + final int startId = msg.arg1; + if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId); + + // Since database is current source of truth, our "active" status + // depends on database state. We always get one final update pass + // once the real actions have finished and persisted their state. + + // TODO: switch to asking real tasks to derive active state + // TODO: handle media scanner timeouts + + final boolean isActive; + synchronized (mDownloads) { + isActive = updateLocked(); + } + + if (msg.what == MSG_FINAL_UPDATE) { + Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive + + "; someone didn't update correctly."); + } + + if (isActive) { + // Still doing useful work, keep service alive. These active + // tasks will trigger another update pass when they're finished. + + // Enqueue delayed update pass to catch finished operations that + // didn't trigger an update pass; these are bugs. + enqueueFinalUpdate(); + + } else { + // No active tasks, and any pending update messages can be + // ignored, since any updates important enough to initiate tasks + // will always be delivered with a new startId. + + if (stopSelfResult(startId)) { + if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped"); + mUpdateThread.quit(); } + } - synchronized (mDownloads) { - long now = mSystemFacade.currentTimeMillis(); - boolean mustScan = false; - keepService = false; - wakeUp = Long.MAX_VALUE; - Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet()); - - Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, - null, null, null, null); - if (cursor == null) { - continue; - } - try { - DownloadInfo.Reader reader = - new DownloadInfo.Reader(getContentResolver(), cursor); - int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); - if (Constants.LOGVV) { - Log.i(Constants.TAG, "number of rows from downloads-db: " + - cursor.getCount()); - } - for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { - long id = cursor.getLong(idColumn); - idsNoLongerInDatabase.remove(id); - DownloadInfo info = mDownloads.get(id); - if (info != null) { - updateDownload(reader, info, now); - } else { - info = insertDownloadLocked(reader, now); - } - - if (info.shouldScanFile() && !scanFile(info, true, false)) { - mustScan = true; - keepService = true; - } - if (info.hasCompletionNotification()) { - keepService = true; - } - long next = info.nextAction(now); - if (next == 0) { - keepService = true; - } else if (next > 0 && next < wakeUp) { - wakeUp = next; - } - } - } finally { - cursor.close(); - } + return true; + } + }; - for (Long id : idsNoLongerInDatabase) { - deleteDownloadLocked(id); - } + /** + * Update {@link #mDownloads} to match {@link DownloadProvider} state. + * Depending on current download state it may enqueue {@link DownloadThread} + * instances, request {@link DownloadScanner} scans, update user-visible + * notifications, and/or schedule future actions with {@link AlarmManager}. + * <p> + * Should only be called from {@link #mUpdateThread} as after being + * requested through {@link #enqueueUpdate()}. + * + * @return If there are active tasks being processed, as of the database + * snapshot taken in this update. + */ + private boolean updateLocked() { + final long now = mSystemFacade.currentTimeMillis(); - // is there a need to start the DownloadService? yes, if there are rows to be - // deleted. - if (!mustScan) { - for (DownloadInfo info : mDownloads.values()) { - if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) { - mustScan = true; - keepService = true; - break; - } - } - } - mNotifier.updateWith(mDownloads.values()); - if (mustScan) { - bindMediaScanner(); - } else { - mMediaScannerConnection.disconnectMediaScanner(); + boolean isActive = false; + long nextActionMillis = Long.MAX_VALUE; + + final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet()); + + final ContentResolver resolver = getContentResolver(); + final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, + null, null, null, null); + try { + final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); + final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); + while (cursor.moveToNext()) { + final long id = cursor.getLong(idColumn); + staleIds.remove(id); + + DownloadInfo info = mDownloads.get(id); + if (info != null) { + updateDownload(reader, info, now); + } else { + info = insertDownloadLocked(reader, now); + } + + if (info.mDeleted) { + // Delete download if requested, but only after cleaning up + if (!TextUtils.isEmpty(info.mMediaProviderUri)) { + resolver.delete(Uri.parse(info.mMediaProviderUri), null, null); } - // look for all rows with deleted flag set and delete the rows from the database - // permanently - for (DownloadInfo info : mDownloads.values()) { - if (info.mDeleted) { - // this row is to be deleted from the database. but does it have - // mediaProviderUri? - if (TextUtils.isEmpty(info.mMediaProviderUri)) { - if (info.shouldScanFile()) { - // initiate rescan of the file to - which will populate - // mediaProviderUri column in this row - if (!scanFile(info, false, true)) { - throw new IllegalStateException("scanFile failed!"); - } - continue; - } - } else { - // yes it has mediaProviderUri column already filled in. - // delete it from MediaProvider database. - getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null, - null); - } - // delete the file - deleteFileIfExists(info.mFileName); - // delete from the downloads db - getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, - Downloads.Impl._ID + " = ? ", - new String[]{String.valueOf(info.mId)}); - } + deleteFileIfExists(info.mFileName); + resolver.delete(info.getAllDownloadsUri(), null, null); + + } else { + // Kick off download task if ready + final boolean activeDownload = info.startDownloadIfReady(mExecutor); + + // Kick off media scan if completed + final boolean activeScan = info.startScanIfReady(mScanner); + + if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) { + Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload + + ", activeScan=" + activeScan); } + + isActive |= activeDownload; + isActive |= activeScan; } + + // Keep track of nearest next action + nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis); } + } finally { + cursor.close(); } - private void bindMediaScanner() { - if (!mMediaScannerConnecting) { - Intent intent = new Intent(); - intent.setClassName("com.android.providers.media", - "com.android.providers.media.MediaScannerService"); - mMediaScannerConnecting = true; - bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE); - } + // Clean up stale downloads that disappeared + for (Long id : staleIds) { + deleteDownloadLocked(id); } - private void scheduleAlarm(long wakeUp) { - AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarms == null) { - Log.e(Constants.TAG, "couldn't get alarm manager"); - return; - } + // Update notifications visible to user + mNotifier.updateWith(mDownloads.values()); + // Set alarm when next action is in future. It's okay if the service + // continues to run in meantime, since it will kick off an update pass. + if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) { if (Constants.LOGV) { - Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms"); + Log.v(TAG, "scheduling start in " + nextActionMillis + "ms"); } - Intent intent = new Intent(Constants.ACTION_RETRY); - intent.setClassName("com.android.providers.downloads", - DownloadReceiver.class.getName()); - alarms.set( - AlarmManager.RTC_WAKEUP, - mSystemFacade.currentTimeMillis() + wakeUp, - PendingIntent.getBroadcast(DownloadService.this, 0, intent, - PendingIntent.FLAG_ONE_SHOT)); + final Intent intent = new Intent(Constants.ACTION_RETRY); + intent.setClass(this, DownloadReceiver.class); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis, + PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)); } + + return isActive; } /** @@ -435,14 +362,14 @@ public class DownloadService extends Service { * download if appropriate. */ private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) { - DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade); + final DownloadInfo info = reader.newDownloadInfo( + this, mSystemFacade, mStorageManager, mNotifier); mDownloads.put(info.mId, info); if (Constants.LOGVV) { Log.v(Constants.TAG, "processing inserted download " + info.mId); } - info.startIfReady(now, mStorageManager); return info; } @@ -450,15 +377,11 @@ public class DownloadService extends Service { * Updates the local copy of the info about a download. */ private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { - int oldVisibility = info.mVisibility; - int oldStatus = info.mStatus; - reader.updateFromDatabase(info); if (Constants.LOGVV) { Log.v(Constants.TAG, "processing updated download " + info.mId + ", status: " + info.mStatus); } - info.startIfReady(now, mStorageManager); } /** @@ -473,88 +396,20 @@ public class DownloadService extends Service { if (Constants.LOGVV) { Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); } - new File(info.mFileName).delete(); + deleteFileIfExists(info.mFileName); } mDownloads.remove(info.mId); } - /** - * Attempts to scan the file if necessary. - * @return true if the file has been properly scanned. - */ - private boolean scanFile(DownloadInfo info, final boolean updateDatabase, - final boolean deleteFile) { - synchronized (this) { - if (mMediaScannerService == null) { - // not bound to mediaservice. but if in the process of connecting to it, wait until - // connection is resolved - while (mMediaScannerConnecting) { - Log.d(Constants.TAG, "waiting for mMediaScannerService service: "); - try { - this.wait(WAIT_TIMEOUT); - } catch (InterruptedException e1) { - throw new IllegalStateException("wait interrupted"); - } - } - } - // do we have mediaservice? - if (mMediaScannerService == null) { - // no available MediaService And not even in the process of connecting to it - return false; - } - if (Constants.LOGV) { - Log.v(Constants.TAG, "Scanning file " + info.mFileName); - } - try { - final Uri key = info.getAllDownloadsUri(); - final long id = info.mId; - mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType, - new IMediaScannerListener.Stub() { - public void scanCompleted(String path, Uri uri) { - if (updateDatabase) { - // Mark this as 'scanned' in the database - // so that it is NOT subject to re-scanning by MediaScanner - // next time this database row row is encountered - ContentValues values = new ContentValues(); - values.put(Constants.MEDIA_SCANNED, 1); - if (uri != null) { - values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, - uri.toString()); - } - getContentResolver().update(key, values, null, null); - } else if (deleteFile) { - if (uri != null) { - // use the Uri returned to delete it from the MediaProvider - getContentResolver().delete(uri, null, null); - } - // delete the file and delete its row from the downloads db - deleteFileIfExists(path); - getContentResolver().delete( - Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, - Downloads.Impl._ID + " = ? ", - new String[]{String.valueOf(id)}); - } - } - }); - return true; - } catch (RemoteException e) { - Log.w(Constants.TAG, "Failed to scan file " + info.mFileName); - return false; - } - } - } - private void deleteFileIfExists(String path) { - try { - if (!TextUtils.isEmpty(path)) { - if (Constants.LOGVV) { - Log.d(TAG, "deleteFileIfExists() deleting " + path); - } - File file = new File(path); - file.delete(); + if (!TextUtils.isEmpty(path)) { + if (Constants.LOGVV) { + Log.d(TAG, "deleteFileIfExists() deleting " + path); + } + final File file = new File(path); + if (file.exists() && !file.delete()) { + Log.w(TAG, "file: '" + path + "' couldn't be deleted"); } - } catch (Exception e) { - Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); } } diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java index 34bc8e34..a0b3e54a 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -16,16 +16,33 @@ package com.android.providers.downloads; +import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST; +import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME; +import static android.provider.Downloads.Impl.STATUS_FILE_ERROR; +import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR; +import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; +import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; +import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.providers.downloads.Constants.TAG; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_MOVED_PERM; +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_PARTIAL; +import static java.net.HttpURLConnection.HTTP_SEE_OTHER; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.drm.DrmManagerClient; +import android.drm.DrmOutputStream; +import android.net.ConnectivityManager; import android.net.INetworkPolicyListener; +import android.net.NetworkInfo; import android.net.NetworkPolicyManager; -import android.net.Proxy; import android.net.TrafficStats; -import android.net.http.AndroidHttpClient; import android.os.FileUtils; import android.os.PowerManager; import android.os.Process; @@ -35,39 +52,51 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.params.ConnRouteParams; +import com.android.providers.downloads.DownloadInfo.NetworkState; import java.io.File; -import java.io.FileNotFoundException; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.SyncFailedException; -import java.net.URI; -import java.net.URISyntaxException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +import libcore.io.IoUtils; /** - * Runs an actual download + * Task which executes a given {@link DownloadInfo}: making network requests, + * persisting data to disk, and updating {@link DownloadProvider}. */ -public class DownloadThread extends Thread { +public class DownloadThread implements Runnable { + + // TODO: bind each download to a specific network interface to avoid state + // checking races once we have ConnectivityManager API + + private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + private static final int HTTP_TEMP_REDIRECT = 307; + + private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS); private final Context mContext; private final DownloadInfo mInfo; private final SystemFacade mSystemFacade; private final StorageManager mStorageManager; - private DrmConvertSession mDrmConvertSession; + private final DownloadNotifier mNotifier; private volatile boolean mPolicyDirty; public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info, - StorageManager storageManager) { + StorageManager storageManager, DownloadNotifier notifier) { mContext = context; mSystemFacade = systemFacade; mInfo = info; mStorageManager = storageManager; + mNotifier = notifier; } /** @@ -86,12 +115,8 @@ public class DownloadThread extends Thread { */ static class State { public String mFilename; - public FileOutputStream mStream; public String mMimeType; - public boolean mCountRetry = false; public int mRetryAfter = 0; - public int mRedirectCount = 0; - public String mNewUri; public boolean mGotData = false; public String mRequestUri; public long mTotalBytes = -1; @@ -100,6 +125,7 @@ public class DownloadThread extends Thread { public boolean mContinuingDownload = false; public long mBytesNotified = 0; public long mTimeLastNotification = 0; + public int mNetworkType = ConnectivityManager.TYPE_NONE; /** Historical bytes/second speed of this download. */ public long mSpeed; @@ -108,6 +134,13 @@ public class DownloadThread extends Thread { /** Bytes transferred since current sample started. */ public long mSpeedSampleBytes; + public long mContentLength = -1; + public String mContentDisposition; + public String mContentLocation; + + public int mRedirectionCount; + public URL mUrl; + public State(DownloadInfo info) { mMimeType = Intent.normalizeMimeType(info.mMimeType); mRequestUri = info.mUri; @@ -115,33 +148,23 @@ public class DownloadThread extends Thread { mTotalBytes = info.mTotalBytes; mCurrentBytes = info.mCurrentBytes; } - } - /** - * State within executeDownload() - */ - private static class InnerState { - public String mHeaderContentLength; - public String mHeaderContentDisposition; - public String mHeaderContentLocation; + public void resetBeforeExecute() { + // Reset any state from previous execution + mContentLength = -1; + mContentDisposition = null; + mContentLocation = null; + mRedirectionCount = 0; + } } - /** - * Raised from methods called by executeDownload() to indicate that the download should be - * retried immediately. - */ - private class RetryDownload extends Throwable {} - - /** - * Executes the download in a separate thread - */ @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); try { runInternal(); } finally { - DownloadHandler.getInstance().dequeueDownload(mInfo.mId); + mNotifier.notifyDownloadSpeed(mInfo.mId, 0); } } @@ -155,9 +178,9 @@ public class DownloadThread extends Thread { } State state = new State(mInfo); - AndroidHttpClient client = null; PowerManager.WakeLock wakeLock = null; int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR; + int numFailed = mInfo.mNumFailed; String errorMsg = null; final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext); @@ -170,36 +193,29 @@ public class DownloadThread extends Thread { // while performing download, register for rules updates netPolicy.registerListener(mPolicyListener); - if (Constants.LOGV) { - Log.v(Constants.TAG, "initiating download for " + mInfo.mUri); - } + Log.i(Constants.TAG, "Initiating download " + mInfo.mId); - client = AndroidHttpClient.newInstance(userAgent(), mContext); + // Remember which network this download started on; used to + // determine if errors were due to network changes. + final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); + if (info != null) { + state.mNetworkType = info.getType(); + } - // network traffic on this thread should be counted against the - // requesting uid, and is tagged with well-known value. + // Network traffic on this thread should be counted against the + // requesting UID, and is tagged with well-known value. TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD); TrafficStats.setThreadStatsUid(mInfo.mUid); - boolean finished = false; - while(!finished) { - Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId); - // Set or unset proxy, which may have changed since last GET request. - // setDefaultProxy() supports null as proxy parameter. - ConnRouteParams.setDefaultProxy(client.getParams(), - Proxy.getPreferredHttpHost(mContext, state.mRequestUri)); - HttpGet request = new HttpGet(state.mRequestUri); - try { - executeDownload(state, client, request); - finished = true; - } catch (RetryDownload exc) { - // fall through - } finally { - request.abort(); - request = null; - } + try { + // TODO: migrate URL sanity checking into client side of API + state.mUrl = new URL(state.mRequestUri); + } catch (MalformedURLException e) { + throw new StopRequestException(STATUS_BAD_REQUEST, e); } + executeDownload(state); + if (Constants.LOGV) { Log.v(Constants.TAG, "download completed for " + mInfo.mUri); } @@ -213,9 +229,37 @@ public class DownloadThread extends Thread { if (Constants.LOGV) { Log.w(Constants.TAG, msg, error); } - finalStatus = error.mFinalStatus; + finalStatus = error.getFinalStatus(); + + // Nobody below our level should request retries, since we handle + // failure counts at this level. + if (finalStatus == STATUS_WAITING_TO_RETRY) { + throw new IllegalStateException("Execution should always throw final error codes"); + } + + // Some errors should be retryable, unless we fail too many times. + if (isStatusRetryable(finalStatus)) { + if (state.mGotData) { + numFailed = 1; + } else { + numFailed += 1; + } + + if (numFailed < Constants.MAX_RETRIES) { + final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); + if (info != null && info.getType() == state.mNetworkType + && info.isConnected()) { + // Underlying network is still intact, use normal backoff + finalStatus = STATUS_WAITING_TO_RETRY; + } else { + // Network changed, retry on any next available + finalStatus = STATUS_WAITING_FOR_NETWORK; + } + } + } + // fall through to finally block - } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions + } catch (Throwable ex) { errorMsg = ex.getMessage(); String msg = "Exception for id " + mInfo.mId + ": " + errorMsg; Log.w(Constants.TAG, msg, ex); @@ -225,14 +269,8 @@ public class DownloadThread extends Thread { TrafficStats.clearThreadStatsTag(); TrafficStats.clearThreadStatsUid(); - if (client != null) { - client.close(); - client = null; - } cleanupDestination(state, finalStatus); - notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter, - state.mGotData, state.mFilename, - state.mNewUri, state.mMimeType, errorMsg); + notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed); netPolicy.unregisterListener(mPolicyListener); @@ -245,16 +283,12 @@ public class DownloadThread extends Thread { } /** - * Fully execute a single download request - setup and send the request, handle the response, - * and transfer the data to the destination file. + * Fully execute a single download request. Setup and send the request, + * handle the response, and transfer the data to the destination file. */ - private void executeDownload(State state, AndroidHttpClient client, HttpGet request) - throws StopRequestException, RetryDownload { - InnerState innerState = new InnerState(); - byte data[] = new byte[Constants.BUFFER_SIZE]; - - setupDestinationFile(state, innerState); - addRequestHeaders(state, request); + private void executeDownload(State state) throws StopRequestException { + state.resetBeforeExecute(); + setupDestinationFile(state); // skip when already finished; remove after fixing race in 5217390 if (state.mCurrentBytes == state.mTotalBytes) { @@ -263,19 +297,136 @@ public class DownloadThread extends Thread { return; } - // check just before sending the request to avoid using an invalid connection at all - checkConnectivity(); - - HttpResponse response = sendRequest(state, client, request); - handleExceptionalStatus(state, innerState, response); + while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) { + // Open connection and follow any redirects until we have a useful + // response with body. + HttpURLConnection conn = null; + try { + checkConnectivity(); + conn = (HttpURLConnection) state.mUrl.openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setConnectTimeout(DEFAULT_TIMEOUT); + conn.setReadTimeout(DEFAULT_TIMEOUT); + + addRequestHeaders(state, conn); + + final int responseCode = conn.getResponseCode(); + switch (responseCode) { + case HTTP_OK: + if (state.mContinuingDownload) { + throw new StopRequestException( + STATUS_CANNOT_RESUME, "Expected partial, but received OK"); + } + processResponseHeaders(state, conn); + transferData(state, conn); + return; + + case HTTP_PARTIAL: + if (!state.mContinuingDownload) { + throw new StopRequestException( + STATUS_CANNOT_RESUME, "Expected OK, but received partial"); + } + transferData(state, conn); + return; + + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_SEE_OTHER: + case HTTP_TEMP_REDIRECT: + final String location = conn.getHeaderField("Location"); + state.mUrl = new URL(state.mUrl, location); + if (responseCode == HTTP_MOVED_PERM) { + // Push updated URL back to database + state.mRequestUri = state.mUrl.toString(); + } + continue; + + case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE: + throw new StopRequestException( + STATUS_CANNOT_RESUME, "Requested range not satisfiable"); + + case HTTP_UNAVAILABLE: + parseRetryAfterHeaders(state, conn); + throw new StopRequestException( + HTTP_UNAVAILABLE, conn.getResponseMessage()); + + case HTTP_INTERNAL_ERROR: + throw new StopRequestException( + HTTP_INTERNAL_ERROR, conn.getResponseMessage()); + + default: + StopRequestException.throwUnhandledHttpError( + responseCode, conn.getResponseMessage()); + } + } catch (IOException e) { + // Trouble with low-level sockets + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); - if (Constants.LOGV) { - Log.v(Constants.TAG, "received response for " + mInfo.mUri); + } finally { + if (conn != null) conn.disconnect(); + } } - processResponseHeaders(state, innerState, response); - InputStream entityStream = openResponseEntity(state, response); - transferData(state, innerState, data, entityStream); + throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects"); + } + + /** + * Transfer data from the given connection to the destination file. + */ + private void transferData(State state, HttpURLConnection conn) throws StopRequestException { + DrmManagerClient drmClient = null; + InputStream in = null; + OutputStream out = null; + FileDescriptor outFd = null; + try { + try { + in = conn.getInputStream(); + } catch (IOException e) { + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e); + } + + try { + if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) { + drmClient = new DrmManagerClient(mContext); + final RandomAccessFile file = new RandomAccessFile( + new File(state.mFilename), "rw"); + out = new DrmOutputStream(drmClient, file, state.mMimeType); + outFd = file.getFD(); + } else { + out = new FileOutputStream(state.mFilename, true); + outFd = ((FileOutputStream) out).getFD(); + } + } catch (IOException e) { + throw new StopRequestException(STATUS_FILE_ERROR, e); + } + + // Start streaming data, periodically watch for pause/cancel + // commands and checking disk space as needed. + transferData(state, in, out); + + try { + if (out instanceof DrmOutputStream) { + ((DrmOutputStream) out).finish(); + } + } catch (IOException e) { + throw new StopRequestException(STATUS_FILE_ERROR, e); + } + + } finally { + if (drmClient != null) { + drmClient.release(); + } + + IoUtils.closeQuietly(in); + + try { + if (out != null) out.flush(); + if (outFd != null) outFd.sync(); + } catch (IOException e) { + } finally { + IoUtils.closeQuietly(out); + } + } } /** @@ -285,40 +436,38 @@ public class DownloadThread extends Thread { // checking connectivity will apply current policy mPolicyDirty = false; - int networkUsable = mInfo.checkCanUseNetwork(); - if (networkUsable != DownloadInfo.NETWORK_OK) { + final NetworkState networkUsable = mInfo.checkCanUseNetwork(); + if (networkUsable != NetworkState.OK) { int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; - if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) { + if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(true); - } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { + } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; mInfo.notifyPauseDueToSize(false); } - throw new StopRequestException(status, - mInfo.getLogMessageForNetworkError(networkUsable)); + throw new StopRequestException(status, networkUsable.name()); } } /** - * Transfer as much data as possible from the HTTP response to the destination file. - * @param data buffer to use to read data - * @param entityStream stream for reading the HTTP response entity + * Transfer as much data as possible from the HTTP response to the + * destination file. */ - private void transferData( - State state, InnerState innerState, byte[] data, InputStream entityStream) + private void transferData(State state, InputStream in, OutputStream out) throws StopRequestException { + final byte data[] = new byte[Constants.BUFFER_SIZE]; for (;;) { - int bytesRead = readFromResponse(state, innerState, data, entityStream); + int bytesRead = readFromResponse(state, data, in); if (bytesRead == -1) { // success, end of stream already reached - handleEndOfStream(state, innerState); + handleEndOfStream(state); return; } state.mGotData = true; - writeDataToDestination(state, data, bytesRead); + writeDataToDestination(state, data, bytesRead, out); state.mCurrentBytes += bytesRead; - reportProgress(state, innerState); + reportProgress(state); if (Constants.LOGVV) { Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for " @@ -332,11 +481,10 @@ public class DownloadThread extends Thread { /** * Called after a successful completion to take any necessary action on the downloaded file. */ - private void finalizeDestinationFile(State state) throws StopRequestException { + private void finalizeDestinationFile(State state) { if (state.mFilename != null) { // make sure the file is readable FileUtils.setPermissions(state.mFilename, 0644, -1, -1); - syncDestination(state); } } @@ -345,11 +493,6 @@ public class DownloadThread extends Thread { * the downloaded file. */ private void cleanupDestination(State state, int finalStatus) { - if (mDrmConvertSession != null) { - finalStatus = mDrmConvertSession.close(state.mFilename); - } - - closeDestination(state); if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) { if (Constants.LOGVV) { Log.d(TAG, "cleanupDestination() deleting " + state.mFilename); @@ -360,53 +503,6 @@ public class DownloadThread extends Thread { } /** - * Sync the destination file to storage. - */ - private void syncDestination(State state) { - FileOutputStream downloadedFileStream = null; - try { - downloadedFileStream = new FileOutputStream(state.mFilename, true); - downloadedFileStream.getFD().sync(); - } catch (FileNotFoundException ex) { - Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex); - } catch (SyncFailedException ex) { - Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex); - } catch (IOException ex) { - Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex); - } catch (RuntimeException ex) { - Log.w(Constants.TAG, "exception while syncing file: ", ex); - } finally { - if(downloadedFileStream != null) { - try { - downloadedFileStream.close(); - } catch (IOException ex) { - Log.w(Constants.TAG, "IOException while closing synced file: ", ex); - } catch (RuntimeException ex) { - Log.w(Constants.TAG, "exception while closing file: ", ex); - } - } - } - } - - /** - * Close the destination output stream. - */ - private void closeDestination(State state) { - try { - // close the file - if (state.mStream != null) { - state.mStream.close(); - state.mStream = null; - } - } catch (IOException ex) { - if (Constants.LOGV) { - Log.v(Constants.TAG, "exception when closing the file after download : " + ex); - } - // nothing can really be done if the file can't be closed - } - } - - /** * Check if the download has been paused or canceled, stopping the request appropriately if it * has been. */ @@ -430,7 +526,7 @@ public class DownloadThread extends Thread { /** * Report download progress through the database if necessary. */ - private void reportProgress(State state, InnerState innerState) { + private void reportProgress(State state) { final long now = SystemClock.elapsedRealtime(); final long sampleDelta = now - state.mSpeedSampleStart; @@ -444,10 +540,13 @@ public class DownloadThread extends Thread { state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4; } + // Only notify once we have a full sample window + if (state.mSpeedSampleStart != 0) { + mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed); + } + state.mSpeedSampleStart = now; state.mSpeedSampleBytes = state.mCurrentBytes; - - DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed); } if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP && @@ -465,37 +564,25 @@ public class DownloadThread extends Thread { * @param data buffer containing the data to write * @param bytesRead how many bytes to write from the buffer */ - private void writeDataToDestination(State state, byte[] data, int bytesRead) + private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out) throws StopRequestException { - for (;;) { + mStorageManager.verifySpaceBeforeWritingToFile( + mInfo.mDestination, state.mFilename, bytesRead); + + boolean forceVerified = false; + while (true) { try { - if (state.mStream == null) { - state.mStream = new FileOutputStream(state.mFilename, true); - } - mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename, - bytesRead); - if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) { - state.mStream.write(data, 0, bytesRead); - } else { - byte[] convertedData = mDrmConvertSession.convert(data, bytesRead); - if (convertedData != null) { - state.mStream.write(convertedData, 0, convertedData.length); - } else { - throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, - "Error converting drm data."); - } - } + out.write(data, 0, bytesRead); return; } catch (IOException ex) { - // couldn't write to file. are we out of space? check. - // TODO this check should only be done once. why is this being done - // in a while(true) loop (see the enclosing statement: for(;;) - if (state.mStream != null) { + // TODO: better differentiate between DRM and disk failures + if (!forceVerified) { + // couldn't write to file. are we out of space? check. mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead); - } - } finally { - if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) { - closeDestination(state); + forceVerified = true; + } else { + throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, + "Failed to write data: " + ex); } } } @@ -505,29 +592,30 @@ public class DownloadThread extends Thread { * Called when we've reached the end of the HTTP response stream, to update the database and * check for consistency. */ - private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException { + private void handleEndOfStream(State state) throws StopRequestException { ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); - if (innerState.mHeaderContentLength == null) { + if (state.mContentLength == -1) { values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes); } mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); - boolean lengthMismatched = (innerState.mHeaderContentLength != null) - && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength)); + final boolean lengthMismatched = (state.mContentLength != -1) + && (state.mCurrentBytes != state.mContentLength); if (lengthMismatched) { if (cannotResume(state)) { - throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, - "mismatched content length"); + throw new StopRequestException(STATUS_CANNOT_RESUME, + "mismatched content length; unable to resume"); } else { - throw new StopRequestException(getFinalStatusForHttpError(state), + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "closed socket before end of file"); } } } private boolean cannotResume(State state) { - return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null; + return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null) + || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType); } /** @@ -536,91 +624,51 @@ public class DownloadThread extends Thread { * @param entityStream stream for reading the HTTP response entity * @return the number of bytes actually read or -1 if the end of the stream has been reached */ - private int readFromResponse(State state, InnerState innerState, byte[] data, - InputStream entityStream) throws StopRequestException { + private int readFromResponse(State state, byte[] data, InputStream entityStream) + throws StopRequestException { try { return entityStream.read(data); } catch (IOException ex) { - logNetworkState(mInfo.mUid); + // TODO: handle stream errors the same as other retries + if ("unexpected end of stream".equals(ex.getMessage())) { + return -1; + } + ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes); mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null); if (cannotResume(state)) { - String message = "while reading response: " + ex.toString() - + ", can't resume interrupted download with no ETag"; - throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME, - message, ex); + throw new StopRequestException(STATUS_CANNOT_RESUME, + "Failed reading response: " + ex + "; unable to resume", ex); } else { - throw new StopRequestException(getFinalStatusForHttpError(state), - "while reading response: " + ex.toString(), ex); + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, + "Failed reading response: " + ex, ex); } } } /** - * Open a stream for the HTTP response entity, handling I/O errors. - * @return an InputStream to read the response entity - */ - private InputStream openResponseEntity(State state, HttpResponse response) - throws StopRequestException { - try { - return response.getEntity().getContent(); - } catch (IOException ex) { - logNetworkState(mInfo.mUid); - throw new StopRequestException(getFinalStatusForHttpError(state), - "while getting entity: " + ex.toString(), ex); - } - } - - private void logNetworkState(int uid) { - if (Constants.LOGX) { - Log.i(Constants.TAG, - "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down")); - } - } - - /** - * Read HTTP response headers and take appropriate action, including setting up the destination - * file and updating the database. + * Prepare target file based on given network response. Derives filename and + * target size as needed. */ - private void processResponseHeaders(State state, InnerState innerState, HttpResponse response) + private void processResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { - if (state.mContinuingDownload) { - // ignore response headers on resume requests - return; - } + // TODO: fallocate the entire file if header gave us specific length - readResponseHeaders(state, innerState, response); - if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) { - mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType); - if (mDrmConvertSession == null) { - throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype " - + state.mMimeType + " can not be converted."); - } - } + readResponseHeaders(state, conn); state.mFilename = Helpers.generateSaveFile( mContext, mInfo.mUri, mInfo.mHint, - innerState.mHeaderContentDisposition, - innerState.mHeaderContentLocation, + state.mContentDisposition, + state.mContentLocation, state.mMimeType, mInfo.mDestination, - (innerState.mHeaderContentLength != null) ? - Long.parseLong(innerState.mHeaderContentLength) : 0, - mInfo.mIsPublicApi, mStorageManager); - try { - state.mStream = new FileOutputStream(state.mFilename); - } catch (FileNotFoundException exc) { - throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, - "while opening destination file: " + exc.toString(), exc); - } - if (Constants.LOGV) { - Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename); - } + state.mContentLength, + mStorageManager); - updateDatabaseFromHeaders(state, innerState); + updateDatabaseFromHeaders(state); // check connectivity again now that we know the total size checkConnectivity(); } @@ -629,7 +677,7 @@ public class DownloadThread extends Thread { * Update necessary database fields based on values of HTTP response headers that have been * read. */ - private void updateDatabaseFromHeaders(State state, InnerState innerState) { + private void updateDatabaseFromHeaders(State state) { ContentValues values = new ContentValues(); values.put(Downloads.Impl._DATA, state.mFilename); if (state.mHeaderETag != null) { @@ -645,219 +693,48 @@ public class DownloadThread extends Thread { /** * Read headers from the HTTP response and store them into local state. */ - private void readResponseHeaders(State state, InnerState innerState, HttpResponse response) + private void readResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException { - Header header = response.getFirstHeader("Content-Disposition"); - if (header != null) { - innerState.mHeaderContentDisposition = header.getValue(); - } - header = response.getFirstHeader("Content-Location"); - if (header != null) { - innerState.mHeaderContentLocation = header.getValue(); - } - if (state.mMimeType == null) { - header = response.getFirstHeader("Content-Type"); - if (header != null) { - state.mMimeType = Intent.normalizeMimeType(header.getValue()); - } - } - header = response.getFirstHeader("ETag"); - if (header != null) { - state.mHeaderETag = header.getValue(); - } - String headerTransferEncoding = null; - header = response.getFirstHeader("Transfer-Encoding"); - if (header != null) { - headerTransferEncoding = header.getValue(); - } - if (headerTransferEncoding == null) { - header = response.getFirstHeader("Content-Length"); - if (header != null) { - innerState.mHeaderContentLength = header.getValue(); - state.mTotalBytes = mInfo.mTotalBytes = - Long.parseLong(innerState.mHeaderContentLength); - } - } else { - // Ignore content-length with transfer-encoding - 2616 4.4 3 - if (Constants.LOGVV) { - Log.v(Constants.TAG, - "ignoring content-length because of xfer-encoding"); - } - } - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Content-Disposition: " + - innerState.mHeaderContentDisposition); - Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength); - Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation); - Log.v(Constants.TAG, "Content-Type: " + state.mMimeType); - Log.v(Constants.TAG, "ETag: " + state.mHeaderETag); - Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding); - } - - boolean noSizeInfo = innerState.mHeaderContentLength == null - && (headerTransferEncoding == null - || !headerTransferEncoding.equalsIgnoreCase("chunked")); - if (!mInfo.mNoIntegrity && noSizeInfo) { - throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, - "can't know size of download, giving up"); - } - } + state.mContentDisposition = conn.getHeaderField("Content-Disposition"); + state.mContentLocation = conn.getHeaderField("Content-Location"); - /** - * Check the HTTP response status and handle anything unusual (e.g. not 200/206). - */ - private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response) - throws StopRequestException, RetryDownload { - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) { - handleServiceUnavailable(state, response); - } - if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) { - handleRedirect(state, response, statusCode); + if (state.mMimeType == null) { + state.mMimeType = Intent.normalizeMimeType(conn.getContentType()); } - if (Constants.LOGV) { - Log.i(Constants.TAG, "recevd_status = " + statusCode + - ", mContinuingDownload = " + state.mContinuingDownload); - } - int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS; - if (statusCode != expectedStatus) { - handleOtherStatus(state, innerState, statusCode); - } - } + state.mHeaderETag = conn.getHeaderField("ETag"); - /** - * Handle a status that we don't know how to deal with properly. - */ - private void handleOtherStatus(State state, InnerState innerState, int statusCode) - throws StopRequestException { - if (statusCode == 416) { - // range request failed. it should never fail. - throw new IllegalStateException("Http Range request failure: totalBytes = " + - state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes); - } - int finalStatus; - if (Downloads.Impl.isStatusError(statusCode)) { - finalStatus = statusCode; - } else if (statusCode >= 300 && statusCode < 400) { - finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT; - } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) { - finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME; + final String transferEncoding = conn.getHeaderField("Transfer-Encoding"); + if (transferEncoding == null) { + state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1); } else { - finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; + Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined"); + state.mContentLength = -1; } - throw new StopRequestException(finalStatus, "http error " + - statusCode + ", mContinuingDownload: " + state.mContinuingDownload); - } - /** - * Handle a 3xx redirect status. - */ - private void handleRedirect(State state, HttpResponse response, int statusCode) - throws StopRequestException, RetryDownload { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP redirect " + statusCode); - } - if (state.mRedirectCount >= Constants.MAX_REDIRECTS) { - throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, - "too many redirects"); - } - Header header = response.getFirstHeader("Location"); - if (header == null) { - return; - } - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Location :" + header.getValue()); - } - - String newUri; - try { - newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString(); - } catch(URISyntaxException ex) { - if (Constants.LOGV) { - Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue() - + " for " + mInfo.mUri); - } - throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, - "Couldn't resolve redirect URI"); - } - ++state.mRedirectCount; - state.mRequestUri = newUri; - if (statusCode == 301 || statusCode == 303) { - // use the new URI for all future requests (should a retry/resume be necessary) - state.mNewUri = newUri; - } - throw new RetryDownload(); - } + state.mTotalBytes = state.mContentLength; + mInfo.mTotalBytes = state.mContentLength; - /** - * Handle a 503 Service Unavailable status by processing the Retry-After header. - */ - private void handleServiceUnavailable(State state, HttpResponse response) - throws StopRequestException { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "got HTTP response code 503"); - } - state.mCountRetry = true; - Header header = response.getFirstHeader("Retry-After"); - if (header != null) { - try { - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Retry-After :" + header.getValue()); - } - state.mRetryAfter = Integer.parseInt(header.getValue()); - if (state.mRetryAfter < 0) { - state.mRetryAfter = 0; - } else { - if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { - state.mRetryAfter = Constants.MIN_RETRY_AFTER; - } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { - state.mRetryAfter = Constants.MAX_RETRY_AFTER; - } - state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); - state.mRetryAfter *= 1000; - } - } catch (NumberFormatException ex) { - // ignored - retryAfter stays 0 in this case. - } - } - throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY, - "got 503 Service Unavailable, will retry later"); - } - - /** - * Send the request to the server, handling any I/O exceptions. - */ - private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request) - throws StopRequestException { - try { - return client.execute(request); - } catch (IllegalArgumentException ex) { - throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR, - "while trying to execute request: " + ex.toString(), ex); - } catch (IOException ex) { - logNetworkState(mInfo.mUid); - throw new StopRequestException(getFinalStatusForHttpError(state), - "while trying to execute request: " + ex.toString(), ex); + final boolean noSizeInfo = state.mContentLength == -1 + && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked")); + if (!mInfo.mNoIntegrity && noSizeInfo) { + throw new StopRequestException(STATUS_CANNOT_RESUME, + "can't know size of download, giving up"); } } - private int getFinalStatusForHttpError(State state) { - int networkUsable = mInfo.checkCanUseNetwork(); - if (networkUsable != DownloadInfo.NETWORK_OK) { - switch (networkUsable) { - case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE: - case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE: - return Downloads.Impl.STATUS_QUEUED_FOR_WIFI; - default: - return Downloads.Impl.STATUS_WAITING_FOR_NETWORK; - } - } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) { - state.mCountRetry = true; - return Downloads.Impl.STATUS_WAITING_TO_RETRY; + private void parseRetryAfterHeaders(State state, HttpURLConnection conn) { + state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1); + if (state.mRetryAfter < 0) { + state.mRetryAfter = 0; } else { - Log.w(Constants.TAG, "reached max retries for " + mInfo.mId); - return Downloads.Impl.STATUS_HTTP_DATA_ERROR; + if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) { + state.mRetryAfter = Constants.MIN_RETRY_AFTER; + } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) { + state.mRetryAfter = Constants.MAX_RETRY_AFTER; + } + state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); + state.mRetryAfter *= 1000; } } @@ -865,8 +742,7 @@ public class DownloadThread extends Thread { * Prepare the destination file to receive data. If the file already exists, we'll set up * appropriately for resumption. */ - private void setupDestinationFile(State state, InnerState innerState) - throws StopRequestException { + private void setupDestinationFile(State state) throws StopRequestException { if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download if (Constants.LOGV) { Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId + @@ -913,15 +789,9 @@ public class DownloadThread extends Thread { Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId + ", and starting with file of length: " + fileLength); } - try { - state.mStream = new FileOutputStream(state.mFilename, true); - } catch (FileNotFoundException exc) { - throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, - "while opening destination for resuming: " + exc.toString(), exc); - } state.mCurrentBytes = (int) fileLength; if (mInfo.mTotalBytes != -1) { - innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes); + state.mContentLength = mInfo.mTotalBytes; } state.mHeaderETag = mInfo.mETag; state.mContinuingDownload = true; @@ -933,30 +803,26 @@ public class DownloadThread extends Thread { } } } - - if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) { - closeDestination(state); - } } /** * Add custom headers for this download to the HTTP request. */ - private void addRequestHeaders(State state, HttpGet request) { + private void addRequestHeaders(State state, HttpURLConnection conn) { for (Pair<String, String> header : mInfo.getHeaders()) { - request.addHeader(header.first, header.second); + conn.addRequestProperty(header.first, header.second); + } + + // Only splice in user agent when not already defined + if (conn.getRequestProperty("User-Agent") == null) { + conn.addRequestProperty("User-Agent", userAgent()); } if (state.mContinuingDownload) { if (state.mHeaderETag != null) { - request.addHeader("If-Match", state.mHeaderETag); - } - request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-"); - if (Constants.LOGV) { - Log.i(Constants.TAG, "Adding Range header: " + - "bytes=" + state.mCurrentBytes + "-"); - Log.i(Constants.TAG, " totalBytes = " + state.mTotalBytes); + conn.addRequestProperty("If-Match", state.mHeaderETag); } + conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-"); } } @@ -964,35 +830,27 @@ public class DownloadThread extends Thread { * Stores information about the completed download, and notifies the initiating application. */ private void notifyDownloadCompleted( - int status, boolean countRetry, int retryAfter, boolean gotData, - String filename, String uri, String mimeType, String errorMsg) { - notifyThroughDatabase( - status, countRetry, retryAfter, gotData, filename, uri, mimeType, - errorMsg); - if (Downloads.Impl.isStatusCompleted(status)) { + State state, int finalStatus, String errorMsg, int numFailed) { + notifyThroughDatabase(state, finalStatus, errorMsg, numFailed); + if (Downloads.Impl.isStatusCompleted(finalStatus)) { mInfo.sendIntentIfRequested(); } } private void notifyThroughDatabase( - int status, boolean countRetry, int retryAfter, boolean gotData, - String filename, String uri, String mimeType, String errorMsg) { + State state, int finalStatus, String errorMsg, int numFailed) { ContentValues values = new ContentValues(); - values.put(Downloads.Impl.COLUMN_STATUS, status); - values.put(Downloads.Impl._DATA, filename); - if (uri != null) { - values.put(Downloads.Impl.COLUMN_URI, uri); - } - values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType); + values.put(Downloads.Impl.COLUMN_STATUS, finalStatus); + values.put(Downloads.Impl._DATA, state.mFilename); + values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType); values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis()); - values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter); - if (!countRetry) { - values.put(Constants.FAILED_CONNECTIONS, 0); - } else if (gotData) { - values.put(Constants.FAILED_CONNECTIONS, 1); - } else { - values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1); + values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed); + values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter); + + if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) { + values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri); } + // save the error message. could be useful to developers. if (!TextUtils.isEmpty(errorMsg)) { values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg); @@ -1021,4 +879,27 @@ public class DownloadThread extends Thread { mPolicyDirty = true; } }; + + public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) { + try { + return Long.parseLong(conn.getHeaderField(field)); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Return if given status is eligible to be treated as + * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}. + */ + public static boolean isStatusRetryable(int status) { + switch (status) { + case STATUS_HTTP_DATA_ERROR: + case HTTP_UNAVAILABLE: + case HTTP_INTERNAL_ERROR: + return true; + default: + return false; + } + } } diff --git a/src/com/android/providers/downloads/DrmConvertSession.java b/src/com/android/providers/downloads/DrmConvertSession.java deleted file mode 100644 index d10edf14..00000000 --- a/src/com/android/providers/downloads/DrmConvertSession.java +++ /dev/null @@ -1,171 +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.providers.downloads; - -import android.content.Context; -import android.drm.DrmConvertedStatus; -import android.drm.DrmManagerClient; -import android.util.Log; -import android.provider.Downloads; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; - - -public class DrmConvertSession { - private DrmManagerClient mDrmClient; - private int mConvertSessionId; - - private DrmConvertSession(DrmManagerClient drmClient, int convertSessionId) { - mDrmClient = drmClient; - mConvertSessionId = convertSessionId; - } - - /** - * Start of converting a file. - * - * @param context The context of the application running the convert session. - * @param mimeType Mimetype of content that shall be converted. - * @return A convert session or null in case an error occurs. - */ - public static DrmConvertSession open(Context context, String mimeType) { - DrmManagerClient drmClient = null; - int convertSessionId = -1; - if (context != null && mimeType != null && !mimeType.equals("")) { - try { - drmClient = new DrmManagerClient(context); - try { - convertSessionId = drmClient.openConvertSession(mimeType); - } catch (IllegalArgumentException e) { - Log.w(Constants.TAG, "Conversion of Mimetype: " + mimeType - + " is not supported.", e); - } catch (IllegalStateException e) { - Log.w(Constants.TAG, "Could not access Open DrmFramework.", e); - } - } catch (IllegalArgumentException e) { - Log.w(Constants.TAG, - "DrmManagerClient instance could not be created, context is Illegal."); - } catch (IllegalStateException e) { - Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly."); - } - } - - if (drmClient == null || convertSessionId < 0) { - return null; - } else { - return new DrmConvertSession(drmClient, convertSessionId); - } - } - /** - * Convert a buffer of data to protected format. - * - * @param buffer Buffer filled with data to convert. - * @param size The number of bytes that shall be converted. - * @return A Buffer filled with converted data, if execution is ok, in all - * other case null. - */ - public byte [] convert(byte[] inBuffer, int size) { - byte[] result = null; - if (inBuffer != null) { - DrmConvertedStatus convertedStatus = null; - try { - if (size != inBuffer.length) { - byte[] buf = new byte[size]; - System.arraycopy(inBuffer, 0, buf, 0, size); - convertedStatus = mDrmClient.convertData(mConvertSessionId, buf); - } else { - convertedStatus = mDrmClient.convertData(mConvertSessionId, inBuffer); - } - - if (convertedStatus != null && - convertedStatus.statusCode == DrmConvertedStatus.STATUS_OK && - convertedStatus.convertedData != null) { - result = convertedStatus.convertedData; - } - } catch (IllegalArgumentException e) { - Log.w(Constants.TAG, "Buffer with data to convert is illegal. Convertsession: " - + mConvertSessionId, e); - } catch (IllegalStateException e) { - Log.w(Constants.TAG, "Could not convert data. Convertsession: " + - mConvertSessionId, e); - } - } else { - throw new IllegalArgumentException("Parameter inBuffer is null"); - } - return result; - } - - /** - * Ends a conversion session of a file. - * - * @param fileName The filename of the converted file. - * @return Downloads.Impl.STATUS_SUCCESS if execution is ok. - * Downloads.Impl.STATUS_FILE_ERROR in case converted file can not - * be accessed. Downloads.Impl.STATUS_NOT_ACCEPTABLE if a problem - * occurs when accessing drm framework. - * Downloads.Impl.STATUS_UNKNOWN_ERROR if a general error occurred. - */ - public int close(String filename) { - DrmConvertedStatus convertedStatus = null; - int result = Downloads.Impl.STATUS_UNKNOWN_ERROR; - if (mDrmClient != null && mConvertSessionId >= 0) { - try { - convertedStatus = mDrmClient.closeConvertSession(mConvertSessionId); - if (convertedStatus == null || - convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK || - convertedStatus.convertedData == null) { - result = Downloads.Impl.STATUS_NOT_ACCEPTABLE; - } else { - RandomAccessFile rndAccessFile = null; - try { - rndAccessFile = new RandomAccessFile(filename, "rw"); - rndAccessFile.seek(convertedStatus.offset); - rndAccessFile.write(convertedStatus.convertedData); - result = Downloads.Impl.STATUS_SUCCESS; - } catch (FileNotFoundException e) { - result = Downloads.Impl.STATUS_FILE_ERROR; - Log.w(Constants.TAG, "File: " + filename + " could not be found.", e); - } catch (IOException e) { - result = Downloads.Impl.STATUS_FILE_ERROR; - Log.w(Constants.TAG, "Could not access File: " + filename + " .", e); - } catch (IllegalArgumentException e) { - result = Downloads.Impl.STATUS_FILE_ERROR; - Log.w(Constants.TAG, "Could not open file in mode: rw", e); - } catch (SecurityException e) { - Log.w(Constants.TAG, "Access to File: " + filename + - " was denied denied by SecurityManager.", e); - } finally { - if (rndAccessFile != null) { - try { - rndAccessFile.close(); - } catch (IOException e) { - result = Downloads.Impl.STATUS_FILE_ERROR; - Log.w(Constants.TAG, "Failed to close File:" + filename - + ".", e); - } - } - } - } - } catch (IllegalStateException e) { - Log.w(Constants.TAG, "Could not close convertsession. Convertsession: " + - mConvertSessionId, e); - } - } - return result; - } -} diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java index 359f6fa4..33205557 100644 --- a/src/com/android/providers/downloads/Helpers.java +++ b/src/com/android/providers/downloads/Helpers.java @@ -17,10 +17,6 @@ package com.android.providers.downloads; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.NetworkInfo; import android.net.Uri; import android.os.Environment; import android.os.SystemClock; @@ -29,6 +25,7 @@ import android.util.Log; import android.webkit.MimeTypeMap; import java.io.File; +import java.io.IOException; import java.util.Random; import java.util.Set; import java.util.regex.Matcher; @@ -44,6 +41,8 @@ public class Helpers { private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + private static final Object sUniqueLock = new Object(); + private Helpers() { } @@ -77,8 +76,10 @@ public class Helpers { String mimeType, int destination, long contentLength, - boolean isPublicApi, StorageManager storageManager) throws StopRequestException { - checkCanHandleDownload(context, mimeType, destination, isPublicApi); + StorageManager storageManager) throws StopRequestException { + if (contentLength < 0) { + contentLength = 0; + } String path; File base = null; if (destination == Downloads.Impl.DESTINATION_FILE_URI) { @@ -90,10 +91,10 @@ public class Helpers { destination); } storageManager.verifySpace(destination, path, contentLength); - path = getFullPath(path, mimeType, destination, base); if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) { path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path); } + path = getFullPath(path, mimeType, destination, base); return path; } @@ -130,47 +131,20 @@ public class Helpers { if (Constants.LOGVV) { Log.v(Constants.TAG, "target file: " + filename + extension); } - return chooseUniqueFilename(destination, filename, extension, recoveryDir); - } - private static void checkCanHandleDownload(Context context, String mimeType, int destination, - boolean isPublicApi) throws StopRequestException { - if (isPublicApi) { - return; - } + synchronized (sUniqueLock) { + final String path = chooseUniqueFilenameLocked( + destination, filename, extension, recoveryDir); - if (destination == Downloads.Impl.DESTINATION_EXTERNAL - || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) { - if (mimeType == null) { - throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, - "external download with no mime type not allowed"); - } - if (!DownloadDrmHelper.isDrmMimeType(context, mimeType)) { - // Check to see if we are allowed to download this file. Only files - // that can be handled by the platform can be downloaded. - // special case DRM files, which we should always allow downloading. - Intent intent = new Intent(Intent.ACTION_VIEW); - - // We can provide data as either content: or file: URIs, - // so allow both. (I think it would be nice if we just did - // everything as content: URIs) - // Actually, right now the download manager's UId restrictions - // prevent use from using content: so it's got to be file: or - // nothing - - PackageManager pm = context.getPackageManager(); - intent.setDataAndType(Uri.fromParts("file", "", null), mimeType); - ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); - //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list); - - if (ri == null) { - if (Constants.LOGV) { - Log.v(Constants.TAG, "no handler found for type " + mimeType); - } - throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, - "no handler found for this download type"); - } + // Claim this filename inside lock to prevent other threads from + // clobbering us. We're not paranoid enough to use O_EXCL. + try { + new File(path).createNewFile(); + } catch (IOException e) { + throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, + "Failed to create target file " + path, e); } + return path; } } @@ -321,7 +295,7 @@ public class Helpers { return extension; } - private static String chooseUniqueFilename(int destination, String filename, + private static String chooseUniqueFilenameLocked(int destination, String filename, String extension, boolean recoveryDir) throws StopRequestException { String fullFilename = filename + extension; if (!new File(fullFilename).exists() @@ -365,14 +339,6 @@ public class Helpers { } /** - * Returns whether the network is available - */ - public static boolean isNetworkAvailable(SystemFacade system, int uid) { - final NetworkInfo info = system.getActiveNetworkInfo(uid); - return info != null && info.isConnected(); - } - - /** * Checks whether the filename looks legitimate */ static boolean isFilenameValid(String filename, File downloadsDataDir) { diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java index 7eca95c9..0d5f5e92 100644 --- a/src/com/android/providers/downloads/OpenHelper.java +++ b/src/com/android/providers/downloads/OpenHelper.java @@ -30,6 +30,8 @@ import android.database.Cursor; import android.net.Uri; import android.provider.Downloads.Impl.RequestHeaders; +import java.io.File; + public class OpenHelper { /** * Build an {@link Intent} to view the download at current {@link Cursor} @@ -47,9 +49,9 @@ public class OpenHelper { } final Uri localUri = getCursorUri(cursor, COLUMN_LOCAL_URI); - final String filename = getCursorString(cursor, COLUMN_LOCAL_FILENAME); + final File file = getCursorFile(cursor, COLUMN_LOCAL_FILENAME); String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE); - mimeType = DownloadDrmHelper.getOriginalMimeType(context, filename, mimeType); + mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType); final Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -122,4 +124,8 @@ public class OpenHelper { private static long getCursorLong(Cursor cursor, String column) { return cursor.getLong(cursor.getColumnIndexOrThrow(column)); } + + private static File getCursorFile(Cursor cursor, String column) { + return new File(cursor.getString(cursor.getColumnIndexOrThrow(column))); + } } diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java index 228c7165..fa4f3488 100644 --- a/src/com/android/providers/downloads/RealSystemFacade.java +++ b/src/com/android/providers/downloads/RealSystemFacade.java @@ -32,10 +32,12 @@ class RealSystemFacade implements SystemFacade { mContext = context; } + @Override public long currentTimeMillis() { return System.currentTimeMillis(); } + @Override public NetworkInfo getActiveNetworkInfo(int uid) { ConnectivityManager connectivity = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -57,6 +59,7 @@ class RealSystemFacade implements SystemFacade { return conn.isActiveNetworkMetered(); } + @Override public boolean isNetworkRoaming() { ConnectivityManager connectivity = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -74,6 +77,7 @@ class RealSystemFacade implements SystemFacade { return isRoaming; } + @Override public Long getMaxBytesOverMobile() { return DownloadManager.getMaxBytesOverMobile(mContext); } @@ -92,9 +96,4 @@ class RealSystemFacade implements SystemFacade { public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; } - - @Override - public void startThread(Thread thread) { - thread.start(); - } } diff --git a/src/com/android/providers/downloads/StopRequestException.java b/src/com/android/providers/downloads/StopRequestException.java index 0ccf53cb..a2b642d8 100644 --- a/src/com/android/providers/downloads/StopRequestException.java +++ b/src/com/android/providers/downloads/StopRequestException.java @@ -15,6 +15,9 @@ */ package com.android.providers.downloads; +import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; +import static android.provider.Downloads.Impl.STATUS_UNHANDLED_REDIRECT; + /** * Raised to indicate that the current request should be stopped immediately. * @@ -23,15 +26,36 @@ package com.android.providers.downloads; * URI, headers, or destination filename. */ class StopRequestException extends Exception { - public int mFinalStatus; + private final int mFinalStatus; public StopRequestException(int finalStatus, String message) { super(message); mFinalStatus = finalStatus; } - public StopRequestException(int finalStatus, String message, Throwable throwable) { - super(message, throwable); + public StopRequestException(int finalStatus, Throwable t) { + super(t); + mFinalStatus = finalStatus; + } + + public StopRequestException(int finalStatus, String message, Throwable t) { + super(message, t); mFinalStatus = finalStatus; } + + public int getFinalStatus() { + return mFinalStatus; + } + + public static StopRequestException throwUnhandledHttpError(int code, String message) + throws StopRequestException { + final String error = "Unhandled HTTP response: " + code + " " + message; + if (code >= 400 && code < 600) { + throw new StopRequestException(code, error); + } else if (code >= 300 && code < 400) { + throw new StopRequestException(STATUS_UNHANDLED_REDIRECT, error); + } else { + throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, error); + } + } } diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java index 915d141b..8ca17300 100644 --- a/src/com/android/providers/downloads/StorageManager.java +++ b/src/com/android/providers/downloads/StorageManager.java @@ -71,12 +71,6 @@ class StorageManager { */ private final File mDownloadDataDir; - /** the Singleton instance of this class. - * TODO: once DownloadService is refactored into a long-living object, there is no need - * for this Singleton'ing. - */ - private static StorageManager sSingleton = null; - /** how often do we need to perform checks on space to make sure space is available */ private static final int FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY = 1024 * 1024; // 1MB private int mBytesDownloadedSinceLastCheckOnSpace = 0; @@ -84,19 +78,9 @@ class StorageManager { /** misc members */ private final Context mContext; - /** - * maintains Singleton instance of this class - */ - synchronized static StorageManager getInstance(Context context) { - if (sSingleton == null) { - sSingleton = new StorageManager(context); - } - return sSingleton; - } - - private StorageManager(Context context) { // constructor is private + public StorageManager(Context context) { mContext = context; - mDownloadDataDir = context.getCacheDir(); + mDownloadDataDir = getDownloadDataDirectory(context); mExternalStorageDir = Environment.getExternalStorageDirectory(); mSystemCacheDir = Environment.getDownloadCacheDirectory(); startThreadToCleanupDatabaseAndPurgeFileSystem(); @@ -308,6 +292,10 @@ class StorageManager { return mDownloadDataDir; } + public static File getDownloadDataDirectory(Context context) { + return context.getCacheDir(); + } + /** * Deletes purgeable files from the cache partition. This also deletes * the matching database entries. Files are deleted in LRU order until diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java index fda97e08..15fc31f9 100644 --- a/src/com/android/providers/downloads/SystemFacade.java +++ b/src/com/android/providers/downloads/SystemFacade.java @@ -61,9 +61,4 @@ interface SystemFacade { * Returns true if the specified UID owns the specified package name. */ public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException; - - /** - * Start a thread. - */ - public void startThread(Thread thread); } |