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
17package com.android.providers.downloads;
18
19import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20import static com.android.providers.downloads.Constants.LOGV;
21import static com.android.providers.downloads.Constants.TAG;
22
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.media.MediaScannerConnection;
28import android.media.MediaScannerConnection.MediaScannerConnectionClient;
29import android.net.Uri;
30import android.os.SystemClock;
31import android.provider.Downloads;
32import android.util.Log;
33
34import com.android.internal.annotations.GuardedBy;
35import com.google.common.collect.Maps;
36
37import java.util.HashMap;
38import java.util.concurrent.CountDownLatch;
39import java.util.concurrent.TimeUnit;
40
41/**
42 * Manages asynchronous scanning of completed downloads.
43 */
44public 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}
182