1925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey/*
2925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * Copyright (C) 2013 The Android Open Source Project
3925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey *
4925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * Licensed under the Apache License, Version 2.0 (the "License");
5925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * you may not use this file except in compliance with the License.
6925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * You may obtain a copy of the License at
7925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey *
8925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey *      http://www.apache.org/licenses/LICENSE-2.0
9925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey *
10925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * Unless required by applicable law or agreed to in writing, software
11925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * distributed under the License is distributed on an "AS IS" BASIS,
12925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * See the License for the specific language governing permissions and
14925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * limitations under the License.
15925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey */
16925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
17925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeypackage com.android.providers.downloads;
18925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
19925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport static android.text.format.DateUtils.MINUTE_IN_MILLIS;
20925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport static com.android.providers.downloads.Constants.LOGV;
21925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport static com.android.providers.downloads.Constants.TAG;
22925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
23925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.content.ContentResolver;
24925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.content.ContentUris;
25925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.content.ContentValues;
26925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.content.Context;
27925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.media.MediaScannerConnection;
28925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.media.MediaScannerConnection.MediaScannerConnectionClient;
29925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.net.Uri;
30925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.os.SystemClock;
31925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport android.provider.Downloads;
329b731a5521f569c91aeb419d43fa098a34cf78cbDoug Zongkerimport android.util.Log;
33925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
34925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport com.android.internal.annotations.GuardedBy;
35925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport com.google.common.collect.Maps;
36925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
37925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeyimport java.util.HashMap;
383a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkeyimport java.util.concurrent.CountDownLatch;
393a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkeyimport java.util.concurrent.TimeUnit;
40925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
41925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey/**
42925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * Manages asynchronous scanning of completed downloads.
43925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey */
44925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeypublic class DownloadScanner implements MediaScannerConnectionClient {
45925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
46925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
47925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private final Context mContext;
48925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private final MediaScannerConnection mConnection;
49925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
50925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private static class ScanRequest {
51925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final long id;
52925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final String path;
53925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final String mimeType;
54925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final long requestRealtime;
55925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
56925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public ScanRequest(long id, String path, String mimeType) {
57925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.id = id;
58925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.path = path;
59925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.mimeType = mimeType;
60925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.requestRealtime = SystemClock.elapsedRealtime();
61925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
62925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
63925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public void exec(MediaScannerConnection conn) {
64925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            conn.scanFile(path, mimeType);
65925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
66925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
67925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
68925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    @GuardedBy("mConnection")
69925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
70925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
713a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private CountDownLatch mLatch;
723a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
73925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public DownloadScanner(Context context) {
74925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        mContext = context;
75925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        mConnection = new MediaScannerConnection(context, this);
76925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
77925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
783a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    public static void requestScanBlocking(Context context, DownloadInfo info) {
79053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey        requestScanBlocking(context, info.mId, info.mFileName, info.mMimeType);
80053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey    }
81053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey
82053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey    public static void requestScanBlocking(Context context, long id, String path, String mimeType) {
833a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final DownloadScanner scanner = new DownloadScanner(context);
843a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        scanner.mLatch = new CountDownLatch(1);
85053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey        scanner.requestScan(new ScanRequest(id, path, mimeType));
863a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        try {
873a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            scanner.mLatch.await(SCAN_TIMEOUT, TimeUnit.MILLISECONDS);
883a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        } catch (InterruptedException e) {
893a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            Thread.currentThread().interrupt();
903a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        } finally {
913a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            scanner.shutdown();
923a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        }
933a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    }
943a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
95925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    /**
96925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * Check if requested scans are still pending. Scans may timeout after an
97925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * internal duration.
98925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     */
99925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public boolean hasPendingScans() {
100925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
101925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            if (mPending.isEmpty()) {
102925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                return false;
103925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            } else {
104925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                // Check if pending scans have timed out
105925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                final long nowRealtime = SystemClock.elapsedRealtime();
106925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                for (ScanRequest req : mPending.values()) {
107925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                    if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
108925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                        return true;
109925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                    }
110925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                }
111925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                return false;
112925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            }
113925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
114925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
115925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
116925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    /**
117925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * Request that given {@link DownloadInfo} be scanned at some point in
118925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * future. Enqueues the request to be scanned asynchronously.
119925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     *
120925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * @see #hasPendingScans()
121925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     */
122053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey    public void requestScan(ScanRequest req) {
123053674aa4d0ecd7e37dadb8ee37a9cafbdf2f16cJeff Sharkey        if (LOGV) Log.v(TAG, "requestScan() for " + req.path);
124925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
125925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            mPending.put(req.path, req);
126925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
127925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            if (mConnection.isConnected()) {
128925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                req.exec(mConnection);
129925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            } else {
130925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                mConnection.connect();
131925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            }
132925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
133925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
134925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
135925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void shutdown() {
136925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        mConnection.disconnect();
137925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
138925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
139925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    @Override
140925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void onMediaScannerConnected() {
141925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
142925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            for (ScanRequest req : mPending.values()) {
143925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                req.exec(mConnection);
144925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            }
145925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
146925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
147925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
148925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    @Override
149925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void onScanCompleted(String path, Uri uri) {
150925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final ScanRequest req;
151925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
152925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            req = mPending.remove(path);
153925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
154925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        if (req == null) {
1559b731a5521f569c91aeb419d43fa098a34cf78cbDoug Zongker            Log.w(TAG, "Missing request for path " + path);
156925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            return;
157925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
158925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
159925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        // Update scanned column, which will kick off a database update pass,
160925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        // eventually deciding if overall service is ready for teardown.
161925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final ContentValues values = new ContentValues();
162925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
163925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        if (uri != null) {
164925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
165925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
166925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
167925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final ContentResolver resolver = mContext.getContentResolver();
168925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final Uri downloadUri = ContentUris.withAppendedId(
169925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
170925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final int rows = resolver.update(downloadUri, values, null, null);
171925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        if (rows == 0) {
172925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            // Local row disappeared during scan; download was probably deleted
173925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            // so clean up now-orphaned media entry.
174925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            resolver.delete(uri, null, null);
175925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
1763a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
1773a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        if (mLatch != null) {
1783a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            mLatch.countDown();
1793a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        }
180925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
181925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey}
182