/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.providers.downloads; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.providers.downloads.Constants.LOGV; import static com.android.providers.downloads.Constants.TAG; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.net.Uri; import android.os.SystemClock; import android.provider.Downloads; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.google.common.collect.Maps; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Manages asynchronous scanning of completed downloads. */ public class DownloadScanner implements MediaScannerConnectionClient { private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS; private final Context mContext; private final MediaScannerConnection mConnection; private static class ScanRequest { public final long id; public final String path; public final String mimeType; public final long requestRealtime; public ScanRequest(long id, String path, String mimeType) { this.id = id; this.path = path; this.mimeType = mimeType; this.requestRealtime = SystemClock.elapsedRealtime(); } public void exec(MediaScannerConnection conn) { conn.scanFile(path, mimeType); } } @GuardedBy("mConnection") private HashMap mPending = Maps.newHashMap(); private CountDownLatch mLatch; public DownloadScanner(Context context) { mContext = context; mConnection = new MediaScannerConnection(context, this); } public static void requestScanBlocking(Context context, DownloadInfo info) { requestScanBlocking(context, info.mId, info.mFileName, info.mMimeType); } public static void requestScanBlocking(Context context, long id, String path, String mimeType) { final DownloadScanner scanner = new DownloadScanner(context); scanner.mLatch = new CountDownLatch(1); scanner.requestScan(new ScanRequest(id, path, mimeType)); try { scanner.mLatch.await(SCAN_TIMEOUT, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { scanner.shutdown(); } } /** * Check if requested scans are still pending. Scans may timeout after an * internal duration. */ public boolean hasPendingScans() { synchronized (mConnection) { if (mPending.isEmpty()) { return false; } else { // Check if pending scans have timed out final long nowRealtime = SystemClock.elapsedRealtime(); for (ScanRequest req : mPending.values()) { if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) { return true; } } return false; } } } /** * Request that given {@link DownloadInfo} be scanned at some point in * future. Enqueues the request to be scanned asynchronously. * * @see #hasPendingScans() */ public void requestScan(ScanRequest req) { if (LOGV) Log.v(TAG, "requestScan() for " + req.path); synchronized (mConnection) { mPending.put(req.path, req); if (mConnection.isConnected()) { req.exec(mConnection); } else { mConnection.connect(); } } } public void shutdown() { mConnection.disconnect(); } @Override public void onMediaScannerConnected() { synchronized (mConnection) { for (ScanRequest req : mPending.values()) { req.exec(mConnection); } } } @Override public void onScanCompleted(String path, Uri uri) { final ScanRequest req; synchronized (mConnection) { req = mPending.remove(path); } if (req == null) { Log.w(TAG, "Missing request for path " + path); return; } // Update scanned column, which will kick off a database update pass, // eventually deciding if overall service is ready for teardown. final ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1); if (uri != null) { values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString()); } final ContentResolver resolver = mContext.getContentResolver(); final Uri downloadUri = ContentUris.withAppendedId( Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id); final int rows = resolver.update(downloadUri, values, null, null); if (rows == 0) { // Local row disappeared during scan; download was probably deleted // so clean up now-orphaned media entry. resolver.delete(uri, null, null); } if (mLatch != null) { mLatch.countDown(); } } }