• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ScanRequest(long id, String path, String mimeType)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 
exec(MediaScannerConnection conn)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 
DownloadScanner(Context context)73     public DownloadScanner(Context context) {
74         mContext = context;
75         mConnection = new MediaScannerConnection(context, this);
76     }
77 
requestScanBlocking(Context context, DownloadInfo info)78     public static void requestScanBlocking(Context context, DownloadInfo info) {
79         requestScanBlocking(context, info.mId, info.mFileName, info.mMimeType);
80     }
81 
requestScanBlocking(Context context, long id, String path, String mimeType)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      */
hasPendingScans()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      */
requestScan(ScanRequest req)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 
shutdown()135     public void shutdown() {
136         mConnection.disconnect();
137     }
138 
139     @Override
onMediaScannerConnected()140     public void onMediaScannerConnected() {
141         synchronized (mConnection) {
142             for (ScanRequest req : mPending.values()) {
143                 req.exec(mConnection);
144             }
145         }
146     }
147 
148     @Override
onScanCompleted(String path, Uri uri)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         // File got deleted while waiting for it to be mediascanned.
160         if (uri == null) {
161             if (mLatch != null) {
162                 mLatch.countDown();
163             }
164             return;
165         }
166 
167         // Update scanned column, which will kick off a database update pass,
168         // eventually deciding if overall service is ready for teardown.
169         final ContentValues values = new ContentValues();
170         values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
171         values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
172 
173         final ContentResolver resolver = mContext.getContentResolver();
174         final Uri downloadUri = ContentUris.withAppendedId(
175                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
176         final int rows = resolver.update(downloadUri, values, null, null);
177         if (rows == 0) {
178             // Local row disappeared during scan; download was probably deleted
179             // so clean up now-orphaned media entry.
180             resolver.delete(uri, null, null);
181         }
182 
183         if (mLatch != null) {
184             mLatch.countDown();
185         }
186     }
187 }
188