Home | History | Annotate | Download | only in downloads
      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 
     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 
     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 
     73     public DownloadScanner(Context context) {
     74         mContext = context;
     75         mConnection = new MediaScannerConnection(context, this);
     76     }
     77 
     78     public static void requestScanBlocking(Context context, DownloadInfo info) {
     79         requestScanBlocking(context, info.mId, info.mFileName, info.mMimeType);
     80     }
     81 
     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      */
     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      */
    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 
    135     public void shutdown() {
    136         mConnection.disconnect();
    137     }
    138 
    139     @Override
    140     public void onMediaScannerConnected() {
    141         synchronized (mConnection) {
    142             for (ScanRequest req : mPending.values()) {
    143                 req.exec(mConnection);
    144             }
    145         }
    146     }
    147 
    148     @Override
    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         // Update scanned column, which will kick off a database update pass,
    160         // eventually deciding if overall service is ready for teardown.
    161         final ContentValues values = new ContentValues();
    162         values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
    163         if (uri != null) {
    164             values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
    165         }
    166 
    167         final ContentResolver resolver = mContext.getContentResolver();
    168         final Uri downloadUri = ContentUris.withAppendedId(
    169                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
    170         final int rows = resolver.update(downloadUri, values, null, null);
    171         if (rows == 0) {
    172             // Local row disappeared during scan; download was probably deleted
    173             // so clean up now-orphaned media entry.
    174             resolver.delete(uri, null, null);
    175         }
    176 
    177         if (mLatch != null) {
    178             mLatch.countDown();
    179         }
    180     }
    181 }
    182