37f511430ec81455eee04a6435e25d06c44afe6c
[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         final DownloadScanner scanner = new DownloadScanner(context);
80         scanner.mLatch = new CountDownLatch(1);
81         scanner.requestScan(info);
82         try {
83             scanner.mLatch.await(SCAN_TIMEOUT, TimeUnit.MILLISECONDS);
84         } catch (InterruptedException e) {
85             Thread.currentThread().interrupt();
86         } finally {
87             scanner.shutdown();
88         }
89     }
90
91     /**
92      * Check if requested scans are still pending. Scans may timeout after an
93      * internal duration.
94      */
95     public boolean hasPendingScans() {
96         synchronized (mConnection) {
97             if (mPending.isEmpty()) {
98                 return false;
99             } else {
100                 // Check if pending scans have timed out
101                 final long nowRealtime = SystemClock.elapsedRealtime();
102                 for (ScanRequest req : mPending.values()) {
103                     if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
104                         return true;
105                     }
106                 }
107                 return false;
108             }
109         }
110     }
111
112     /**
113      * Request that given {@link DownloadInfo} be scanned at some point in
114      * future. Enqueues the request to be scanned asynchronously.
115      *
116      * @see #hasPendingScans()
117      */
118     public void requestScan(DownloadInfo info) {
119         if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName);
120         synchronized (mConnection) {
121             final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType);
122             mPending.put(req.path, req);
123
124             if (mConnection.isConnected()) {
125                 req.exec(mConnection);
126             } else {
127                 mConnection.connect();
128             }
129         }
130     }
131
132     public void shutdown() {
133         mConnection.disconnect();
134     }
135
136     @Override
137     public void onMediaScannerConnected() {
138         synchronized (mConnection) {
139             for (ScanRequest req : mPending.values()) {
140                 req.exec(mConnection);
141             }
142         }
143     }
144
145     @Override
146     public void onScanCompleted(String path, Uri uri) {
147         final ScanRequest req;
148         synchronized (mConnection) {
149             req = mPending.remove(path);
150         }
151         if (req == null) {
152             Log.w(TAG, "Missing request for path " + path);
153             return;
154         }
155
156         // Update scanned column, which will kick off a database update pass,
157         // eventually deciding if overall service is ready for teardown.
158         final ContentValues values = new ContentValues();
159         values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
160         if (uri != null) {
161             values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
162         }
163
164         final ContentResolver resolver = mContext.getContentResolver();
165         final Uri downloadUri = ContentUris.withAppendedId(
166                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
167         final int rows = resolver.update(downloadUri, values, null, null);
168         if (rows == 0) {
169             // Local row disappeared during scan; download was probably deleted
170             // so clean up now-orphaned media entry.
171             resolver.delete(uri, null, null);
172         }
173
174         if (mLatch != null) {
175             mLatch.countDown();
176         }
177     }
178 }