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