summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2012-11-12 16:50:17 -0800
committerJeff Sharkey <jsharkey@android.com>2012-11-14 16:21:54 -0800
commit52b703c5d0c4cff72bafdec0e2229368d3cc20d0 (patch)
tree0f5674a22b1113f32927f1585a8dc1adc9d2ea7b
parenta40a349c0107660bfb4004467550725a3ca3ec97 (diff)
downloadandroid_packages_providers_DownloadProvider-52b703c5d0c4cff72bafdec0e2229368d3cc20d0.tar.gz
android_packages_providers_DownloadProvider-52b703c5d0c4cff72bafdec0e2229368d3cc20d0.tar.bz2
android_packages_providers_DownloadProvider-52b703c5d0c4cff72bafdec0e2229368d3cc20d0.zip
Show remaining time in download notifications.
Calculate speed of in-progress downloads and estimate time remaining until completion. Uses a moving average that is weighted 1:1 with the most recent 500ms sample. Funnels timing data to notifications through DownloadHandler. Bug: 6777872 Change-Id: I9155f2979aa330bd1172f63bbfca1d053815cee5
-rw-r--r--res/values/strings.xml3
-rw-r--r--src/com/android/providers/downloads/DownloadHandler.java46
-rw-r--r--src/com/android/providers/downloads/DownloadNotifier.java13
-rw-r--r--src/com/android/providers/downloads/DownloadService.java2
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java37
-rw-r--r--tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java2
6 files changed, 86 insertions, 17 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c607e35f..3a060e2d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -205,6 +205,9 @@
<item quantity="other"><xliff:g id="number">%d</xliff:g> files waiting</item>
</plurals>
+ <!-- Time remaining until download is complete. [CHAR LIMIT=32] -->
+ <string name="download_remaining"><xliff:g id="duration" example="3 minutes">%s</xliff:g> left</string>
+
<!-- Text for a toast appearing when a user clicks on a completed download, informing the user
that there is no application on the device that can open the file that was downloaded
[CHAR LIMIT=200] -->
diff --git a/src/com/android/providers/downloads/DownloadHandler.java b/src/com/android/providers/downloads/DownloadHandler.java
index 29d34700..dff09eb0 100644
--- a/src/com/android/providers/downloads/DownloadHandler.java
+++ b/src/com/android/providers/downloads/DownloadHandler.java
@@ -18,6 +18,9 @@ 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;
@@ -25,31 +28,37 @@ 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>();
- private static final DownloadHandler mDownloadHandler = new DownloadHandler();
+ @GuardedBy("this")
+ private final LongSparseArray<Long> mRemainingMillis = new LongSparseArray<Long>();
+
private final int mMaxConcurrentDownloadsAllowed = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
- static DownloadHandler getInstance() {
- return mDownloadHandler;
+ private static final DownloadHandler sDownloadHandler = new DownloadHandler();
+
+ public static DownloadHandler getInstance() {
+ return sDownloadHandler;
}
- synchronized void enqueueDownload(DownloadInfo info) {
+ 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);
- startDownloadThread();
+ startDownloadThreadLocked();
}
}
- private synchronized void startDownloadThread() {
+ private void startDownloadThreadLocked() {
Iterator<Long> keys = mDownloadsQueue.keySet().iterator();
ArrayList<Long> ids = new ArrayList<Long>();
while (mDownloadsInProgress.size() < mMaxConcurrentDownloadsAllowed && keys.hasNext()) {
@@ -67,21 +76,34 @@ public class DownloadHandler {
}
}
- synchronized boolean hasDownloadInQueue(long id) {
+ public synchronized boolean hasDownloadInQueue(long id) {
return mDownloadsQueue.containsKey(id) || mDownloadsInProgress.containsKey(id);
}
- synchronized void dequeueDownload(long mId) {
- mDownloadsInProgress.remove(mId);
- startDownloadThread();
+ public synchronized void dequeueDownload(long id) {
+ mDownloadsInProgress.remove(id);
+ mRemainingMillis.remove(id);
+ startDownloadThreadLocked();
if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
notifyAll();
}
}
+ public synchronized void setRemainingMillis(long id, long millis) {
+ mRemainingMillis.put(id, millis);
+ }
+
+ /**
+ * Return remaining time until given {@link DownloadInfo} finishes, in
+ * milliseconds, or -1 if unknown.
+ */
+ public synchronized long getRemainingMillis(long id) {
+ return mRemainingMillis.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 {
+ public synchronized void waitUntilDownloadsTerminate() throws InterruptedException {
if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
if (Constants.LOGVV) {
Log.i(TAG, "nothing to wait on");
diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java
index a1805e5e..f6e7a2ee 100644
--- a/src/com/android/providers/downloads/DownloadNotifier.java
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -31,16 +31,15 @@ import android.content.res.Resources;
import android.net.Uri;
import android.provider.Downloads;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.Set;
import javax.annotation.concurrent.GuardedBy;
@@ -160,18 +159,26 @@ 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 remainingMillis = -1;
for (DownloadInfo info : cluster) {
if (info.mTotalBytes != -1) {
current += info.mCurrentBytes;
total += info.mTotalBytes;
+ remainingMillis = Math.max(
+ handler.getRemainingMillis(info.mId), remainingMillis);
}
}
if (total > 0) {
final int percent = (int) ((current * 100) / total);
- // TODO: calculate remaining time based on recent bandwidth
+ if (remainingMillis != -1) {
+ remainingText = res.getString(R.string.download_remaining,
+ DateUtils.formatDuration(remainingMillis));
+ }
percentText = res.getString(R.string.download_percent, percent);
builder.setProgress(100, percent, false);
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 0a16a7d1..5b767a27 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -39,6 +39,7 @@ import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
@@ -72,6 +73,7 @@ public class DownloadService extends Service {
* downloads based on this data, so that it can deal with situation where the data in the
* content provider changes or disappears.
*/
+ @GuardedBy("mDownloads")
private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
/**
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index e74d5c72..2bd3d362 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -29,6 +29,7 @@ import android.net.http.AndroidHttpClient;
import android.os.FileUtils;
import android.os.PowerManager;
import android.os.Process;
+import android.os.SystemClock;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
@@ -100,6 +101,15 @@ public class DownloadThread extends Thread {
public long mBytesNotified = 0;
public long mTimeLastNotification = 0;
+ /** Historical bytes/second speed of this download. */
+ public long mSpeed;
+ /** Time when current sample started. */
+ public long mSpeedSampleStart;
+ /** Bytes transferred since current sample started. */
+ public long mSpeedSampleBytes;
+ /** Estimated time until finished. */
+ public long mRemainingMillis;
+
public State(DownloadInfo info) {
mMimeType = Intent.normalizeMimeType(info.mMimeType);
mRequestUri = info.mUri;
@@ -423,7 +433,32 @@ public class DownloadThread extends Thread {
* Report download progress through the database if necessary.
*/
private void reportProgress(State state, InnerState innerState) {
- long now = mSystemFacade.currentTimeMillis();
+ final long now = SystemClock.elapsedRealtime();
+
+ final long sampleDelta = now - state.mSpeedSampleStart;
+ if (sampleDelta > 500) {
+ final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
+ / sampleDelta;
+
+ if (state.mSpeed == 0) {
+ state.mSpeed = sampleSpeed;
+ } else {
+ state.mSpeed = (state.mSpeed + sampleSpeed) / 2;
+ }
+
+ state.mSpeedSampleStart = now;
+ state.mSpeedSampleBytes = state.mCurrentBytes;
+
+ if (state.mSpeed != 0) {
+ state.mRemainingMillis = ((state.mTotalBytes - state.mCurrentBytes) * 1000)
+ / state.mSpeed;
+ } else {
+ state.mRemainingMillis = -1;
+ }
+
+ DownloadHandler.getInstance().setRemainingMillis(mInfo.mId, state.mRemainingMillis);
+ }
+
if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
ContentValues values = new ContentValues();
diff --git a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
index bbc5c3e0..23d300f8 100644
--- a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
@@ -116,7 +116,7 @@ public class DownloadProviderFunctionalTest extends AbstractDownloadProviderFunc
int rslt = getDownloadStatus(downloadUri);
if (rslt == Downloads.Impl.STATUS_RUNNING || rslt == Downloads.Impl.STATUS_PENDING) {
Log.i(TAG, "status is: " + rslt + ", for: " + downloadUri);
- DownloadHandler.getInstance().WaitUntilDownloadsTerminate();
+ DownloadHandler.getInstance().waitUntilDownloadsTerminate();
Thread.sleep(100);
} else {
done = true;