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 
39 /**
40  * Manages asynchronous scanning of completed downloads.
41  */
42 public class DownloadScanner implements MediaScannerConnectionClient {
43     private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
44 
45     private final Context mContext;
46     private final MediaScannerConnection mConnection;
47 
48     private static class ScanRequest {
49         public final long id;
50         public final String path;
51         public final String mimeType;
52         public final long requestRealtime;
53 
ScanRequest(long id, String path, String mimeType)54         public ScanRequest(long id, String path, String mimeType) {
55             this.id = id;
56             this.path = path;
57             this.mimeType = mimeType;
58             this.requestRealtime = SystemClock.elapsedRealtime();
59         }
60 
exec(MediaScannerConnection conn)61         public void exec(MediaScannerConnection conn) {
62             conn.scanFile(path, mimeType);
63         }
64     }
65 
66     @GuardedBy("mConnection")
67     private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
68 
DownloadScanner(Context context)69     public DownloadScanner(Context context) {
70         mContext = context;
71         mConnection = new MediaScannerConnection(context, this);
72     }
73 
74     /**
75      * Check if requested scans are still pending. Scans may timeout after an
76      * internal duration.
77      */
hasPendingScans()78     public boolean hasPendingScans() {
79         synchronized (mConnection) {
80             if (mPending.isEmpty()) {
81                 return false;
82             } else {
83                 // Check if pending scans have timed out
84                 final long nowRealtime = SystemClock.elapsedRealtime();
85                 for (ScanRequest req : mPending.values()) {
86                     if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
87                         return true;
88                     }
89                 }
90                 return false;
91             }
92         }
93     }
94 
95     /**
96      * Request that given {@link DownloadInfo} be scanned at some point in
97      * future. Enqueues the request to be scanned asynchronously.
98      *
99      * @see #hasPendingScans()
100      */
requestScan(DownloadInfo info)101     public void requestScan(DownloadInfo info) {
102         if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName);
103         synchronized (mConnection) {
104             final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType);
105             mPending.put(req.path, req);
106 
107             if (mConnection.isConnected()) {
108                 req.exec(mConnection);
109             } else {
110                 mConnection.connect();
111             }
112         }
113     }
114 
shutdown()115     public void shutdown() {
116         mConnection.disconnect();
117     }
118 
119     @Override
onMediaScannerConnected()120     public void onMediaScannerConnected() {
121         synchronized (mConnection) {
122             for (ScanRequest req : mPending.values()) {
123                 req.exec(mConnection);
124             }
125         }
126     }
127 
128     @Override
onScanCompleted(String path, Uri uri)129     public void onScanCompleted(String path, Uri uri) {
130         final ScanRequest req;
131         synchronized (mConnection) {
132             req = mPending.remove(path);
133         }
134         if (req == null) {
135             Log.w(TAG, "Missing request for path " + path);
136             return;
137         }
138 
139         // Update scanned column, which will kick off a database update pass,
140         // eventually deciding if overall service is ready for teardown.
141         final ContentValues values = new ContentValues();
142         values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
143         if (uri != null) {
144             values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
145         }
146 
147         final ContentResolver resolver = mContext.getContentResolver();
148         final Uri downloadUri = ContentUris.withAppendedId(
149                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
150         final int rows = resolver.update(downloadUri, values, null, null);
151         if (rows == 0) {
152             // Local row disappeared during scan; download was probably deleted
153             // so clean up now-orphaned media entry.
154             resolver.delete(uri, null, null);
155         }
156     }
157 }
158