From 52b703c5d0c4cff72bafdec0e2229368d3cc20d0 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Mon, 12 Nov 2012 16:50:17 -0800 Subject: 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 --- .../providers/downloads/DownloadHandler.java | 46 ++++++++++++++++------ .../providers/downloads/DownloadNotifier.java | 13 ++++-- .../providers/downloads/DownloadService.java | 2 + .../providers/downloads/DownloadThread.java | 37 ++++++++++++++++- 4 files changed, 82 insertions(+), 16 deletions(-) (limited to 'src') 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 mDownloadsQueue = new LinkedHashMap(); + @GuardedBy("this") private final HashMap mDownloadsInProgress = new HashMap(); - private static final DownloadHandler mDownloadHandler = new DownloadHandler(); + @GuardedBy("this") + private final LongSparseArray mRemainingMillis = new LongSparseArray(); + 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 keys = mDownloadsQueue.keySet().iterator(); ArrayList ids = new ArrayList(); 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 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(); -- cgit v1.2.3