summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/DownloadScanner.java
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-02-11 16:19:39 -0800
committerJeff Sharkey <jsharkey@android.com>2013-02-12 20:24:16 -0800
commit925976230936a5177365dc24b50da8607a9af8d4 (patch)
tree0266d0edd90fbdb967f5450dfba5677da6fa2a9a /src/com/android/providers/downloads/DownloadScanner.java
parent1ad10ce731d1b54692d7d5ee32601e965f503fa4 (diff)
downloadandroid_packages_providers_DownloadProvider-925976230936a5177365dc24b50da8607a9af8d4.tar.gz
android_packages_providers_DownloadProvider-925976230936a5177365dc24b50da8607a9af8d4.tar.bz2
android_packages_providers_DownloadProvider-925976230936a5177365dc24b50da8607a9af8d4.zip
Redesign of DownloadManager update loop.
Previously, the service lifecycle was managed through a large for() loop which was extremely tricky to reason about. This resulted in several race conditions that could leave the service running indefinitely, or terminate it early before tasks had finished. This change redesigns the update loop to be event driven based on database updates, and to collapse mutiple pending update passes. It is much easier to reason about service termination conditions, and it correctly uses startId to handle races during command delivery. Also moves scanner into isolated class, and switches to using public API instead of binding to private interface. Bug: 7638470, 7455406, 7162341 Change-Id: I380e77f5432223b2acb4e819e37f29f98ee4782b
Diffstat (limited to 'src/com/android/providers/downloads/DownloadScanner.java')
-rw-r--r--src/com/android/providers/downloads/DownloadScanner.java157
1 files changed, 157 insertions, 0 deletions
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);
+ }
+ }
+}