Scan completed downloads when requested.
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadScanner.java
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads;
18
19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20 import static com.android.providers.downloads.Constants.LOGV;
21 import static com.android.providers.downloads.Constants.TAG;
22
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.media.MediaScannerConnection;
28 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
29 import android.net.Uri;
30 import android.os.SystemClock;
31 import android.provider.Downloads;
32 import android.util.Log;
33
34 import com.android.internal.annotations.GuardedBy;
35 import com.google.common.collect.Maps;
36
37 import java.util.HashMap;
38 import java.util.concurrent.CountDownLatch;
39 import java.util.concurrent.TimeUnit;
40
41 /**
42  * Manages asynchronous scanning of completed downloads.
43  */
44 public class DownloadScanner implements MediaScannerConnectionClient {
45     private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
46
47     private final Context mContext;
48     private final MediaScannerConnection mConnection;
49
50     private static class ScanRequest {
51         public final long id;
52         public final String path;
53         public final String mimeType;
54         public final long requestRealtime;
55
56         public ScanRequest(long id, String path, String mimeType) {
57             this.id = id;
58             this.path = path;
59             this.mimeType = mimeType;
60             this.requestRealtime = SystemClock.elapsedRealtime();
61         }
62
63         public void exec(MediaScannerConnection conn) {
64             conn.scanFile(path, mimeType);
65         }
66     }
67
68     @GuardedBy("mConnection")
69     private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
70
71     private CountDownLatch mLatch;
72
73     public DownloadScanner(Context context) {
74         mContext = context;
75         mConnection = new MediaScannerConnection(context, this);
76     }
77
78     public static void requestScanBlocking(Context context, DownloadInfo info) {
79         requestScanBlocking(context, info.mId, info.mFileName, info.mMimeType);
80     }
81
82     public static void requestScanBlocking(Context context, long id, String path, String mimeType) {
83         final DownloadScanner scanner = new DownloadScanner(context);
84         scanner.mLatch = new CountDownLatch(1);
85         scanner.requestScan(new ScanRequest(id, path, mimeType));
86         try {
87             scanner.mLatch.await(SCAN_TIMEOUT, TimeUnit.MILLISECONDS);
88         } catch (InterruptedException e) {
89             Thread.currentThread().interrupt();
90         } finally {
91             scanner.shutdown();
92         }
93     }
94
95     /**
96      * Check if requested scans are still pending. Scans may timeout after an
97      * internal duration.
98      */
99     public boolean hasPendingScans() {
100         synchronized (mConnection) {
101             if (mPending.isEmpty()) {
102                 return false;
103             } else {
104                 // Check if pending scans have timed out
105                 final long nowRealtime = SystemClock.elapsedRealtime();
106                 for (ScanRequest req : mPending.values()) {
107                     if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
108                         return true;
109                     }
110                 }
111                 return false;
112             }
113         }
114     }
115
116     /**
117      * Request that given {@link DownloadInfo} be scanned at some point in
118      * future. Enqueues the request to be scanned asynchronously.
119      *
120      * @see #hasPendingScans()
121      */
122     public void requestScan(ScanRequest req) {
123         if (LOGV) Log.v(TAG, "requestScan() for " + req.path);
124         synchronized (mConnection) {
125             mPending.put(req.path, req);
126
127             if (mConnection.isConnected()) {
128                 req.exec(mConnection);
129             } else {
130                 mConnection.connect();
131             }
132         }
133     }
134
135     public void shutdown() {
136         mConnection.disconnect();
137     }
138
139     @Override
140     public void onMediaScannerConnected() {
141         synchronized (mConnection) {
142             for (ScanRequest req : mPending.values()) {
143                 req.exec(mConnection);
144             }
145         }
146     }
147
148     @Override
149     public void onScanCompleted(String path, Uri uri) {
150         final ScanRequest req;
151         synchronized (mConnection) {
152             req = mPending.remove(path);
153         }
154         if (req == null) {
155             Log.w(TAG, "Missing request for path " + path);
156             return;
157         }
158
159         // Update scanned column, which will kick off a database update pass,
160         // eventually deciding if overall service is ready for teardown.
161         final ContentValues values = new ContentValues();
162         values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
163         if (uri != null) {
164             values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
165         }
166
167         final ContentResolver resolver = mContext.getContentResolver();
168         final Uri downloadUri = ContentUris.withAppendedId(
169                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
170         final int rows = resolver.update(downloadUri, values, null, null);
171         if (rows == 0) {
172             // Local row disappeared during scan; download was probably deleted
173             // so clean up now-orphaned media entry.
174             resolver.delete(uri, null, null);
175         }
176
177         if (mLatch != null) {
178             mLatch.countDown();
179         }
180     }
181 }