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;
38925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
39925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey/**
40925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey * Manages asynchronous scanning of completed downloads.
41925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey */
42925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkeypublic class DownloadScanner implements MediaScannerConnectionClient {
43925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
44925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
45925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private final Context mContext;
46925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private final MediaScannerConnection mConnection;
47925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
48925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private static class ScanRequest {
49925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final long id;
50925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final String path;
51925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final String mimeType;
52925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public final long requestRealtime;
53925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
54925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public ScanRequest(long id, String path, String mimeType) {
55925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.id = id;
56925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.path = path;
57925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.mimeType = mimeType;
58925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            this.requestRealtime = SystemClock.elapsedRealtime();
59925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
60925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
61925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        public void exec(MediaScannerConnection conn) {
62925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            conn.scanFile(path, mimeType);
63925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
64925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
65925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
66925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    @GuardedBy("mConnection")
67925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
68925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
69925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public DownloadScanner(Context context) {
70925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        mContext = context;
71925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        mConnection = new MediaScannerConnection(context, this);
72925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
73925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
74925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    /**
75925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * Check if requested scans are still pending. Scans may timeout after an
76925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * internal duration.
77925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     */
78925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public boolean hasPendingScans() {
79925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
80925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            if (mPending.isEmpty()) {
81925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                return false;
82925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            } else {
83925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                // Check if pending scans have timed out
84925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                final long nowRealtime = SystemClock.elapsedRealtime();
85925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                for (ScanRequest req : mPending.values()) {
86925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                    if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
87925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                        return true;
88925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                    }
89925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                }
90925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                return false;
91925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            }
92925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
93925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
94925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
95925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    /**
96925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * Request that given {@link DownloadInfo} be scanned at some point in
97925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * future. Enqueues the request to be scanned asynchronously.
98925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     *
99925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     * @see #hasPendingScans()
100925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey     */
101925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void requestScan(DownloadInfo info) {
1029b731a5521f569c91aeb419d43fa098a34cf78cbDoug Zongker        if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName);
103925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
104925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType);
105925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            mPending.put(req.path, req);
106925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
107925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            if (mConnection.isConnected()) {
108925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                req.exec(mConnection);
109925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            } else {
110925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                mConnection.connect();
111925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            }
112925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
113925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
114925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
115925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void shutdown() {
116925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        mConnection.disconnect();
117925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
118925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
119925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    @Override
120925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void onMediaScannerConnected() {
121925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
122925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            for (ScanRequest req : mPending.values()) {
123925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                req.exec(mConnection);
124925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            }
125925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
126925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
127925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
128925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    @Override
129925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    public void onScanCompleted(String path, Uri uri) {
130925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final ScanRequest req;
131925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        synchronized (mConnection) {
132925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            req = mPending.remove(path);
133925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
134925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        if (req == null) {
1359b731a5521f569c91aeb419d43fa098a34cf78cbDoug Zongker            Log.w(TAG, "Missing request for path " + path);
136925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            return;
137925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
138925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
139925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        // Update scanned column, which will kick off a database update pass,
140925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        // eventually deciding if overall service is ready for teardown.
141925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final ContentValues values = new ContentValues();
142925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
143925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        if (uri != null) {
144925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
145925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
146925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey
147925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final ContentResolver resolver = mContext.getContentResolver();
148925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final Uri downloadUri = ContentUris.withAppendedId(
149925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey                Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
150925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        final int rows = resolver.update(downloadUri, values, null, null);
151925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        if (rows == 0) {
152925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            // Local row disappeared during scan; download was probably deleted
153925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            // so clean up now-orphaned media entry.
154925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey            resolver.delete(uri, null, null);
155925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey        }
156925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey    }
157925976230936a5177365dc24b50da8607a9af8d4Jeff Sharkey}
158