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