/*
* 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.novoda.downloadmanager.lib;
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 com.novoda.downloadmanager.lib.logger.LLog;
import java.util.HashMap;
import java.util.Map;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
/**
* Manages asynchronous scanning of completed downloads.
*/
class DownloadScanner implements MediaScannerConnectionClient {
private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
private final ContentResolver resolver;
private final MediaScannerConnection mediaScannerConnection;
private final DownloadsUriProvider downloadsUriProvider;
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("mediaScannerConnection")
private Map<String, ScanRequest> pendingRequests = new HashMap<>();
public DownloadScanner(ContentResolver resolver, Context context, DownloadsUriProvider downloadsUriProvider) {
this.resolver = resolver;
this.mediaScannerConnection = new MediaScannerConnection(context, this);
this.downloadsUriProvider = downloadsUriProvider;
}
/**
* Check if requested scans are still pending. Scans may timeout after an
* internal duration.
*/
public boolean hasPendingScans() {
synchronized (mediaScannerConnection) {
if (pendingRequests.isEmpty()) {
return false;
} else {
// Check if pending scans have timed out
final long nowRealtime = SystemClock.elapsedRealtime();
for (ScanRequest req : pendingRequests.values()) {
if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
return true;
}
}
return false;
}
}
}
/**
* Request that given {@link FileDownloadInfo} be scanned at some point in
* future. Enqueues the request to be scanned asynchronously.
*
* @see #hasPendingScans()
*/
public void requestScan(FileDownloadInfo info) {
LLog.v("requestScan() for " + info.getFileName());
synchronized (mediaScannerConnection) {
final ScanRequest req = new ScanRequest(info.getId(), info.getFileName(), info.getMimeType());
pendingRequests.put(req.path, req);
if (mediaScannerConnection.isConnected()) {
req.exec(mediaScannerConnection);
} else {
mediaScannerConnection.connect();
}
}
}
public void shutdown() {
mediaScannerConnection.disconnect();
}
@Override
public void onMediaScannerConnected() {
synchronized (mediaScannerConnection) {
for (ScanRequest req : pendingRequests.values()) {
req.exec(mediaScannerConnection);
}
}
}
@Override
public void onScanCompleted(String path, Uri uri) {
final ScanRequest req;
synchronized (mediaScannerConnection) {
req = pendingRequests.remove(path);
}
if (req == null) {
LLog.w("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(DownloadContract.Downloads.COLUMN_MEDIA_SCANNED, 1);
if (uri != null) {
values.put(DownloadContract.Downloads.COLUMN_MEDIAPROVIDER_URI, uri.toString());
}
final Uri downloadUri = ContentUris.withAppendedId(
downloadsUriProvider.getAllDownloadsUri(), 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);
}
}
}