Home | History | Annotate | Download | only in downloads
      1 /*
      2  * Copyright (C) 2014 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 com.android.providers.downloads.Constants.TAG;
     20 import static com.android.providers.downloads.StorageUtils.listFilesRecursive;
     21 
     22 import android.app.DownloadManager;
     23 import android.app.job.JobInfo;
     24 import android.app.job.JobParameters;
     25 import android.app.job.JobScheduler;
     26 import android.app.job.JobService;
     27 import android.content.ComponentName;
     28 import android.content.ContentResolver;
     29 import android.content.ContentUris;
     30 import android.content.Context;
     31 import android.database.Cursor;
     32 import android.os.Environment;
     33 import android.provider.Downloads;
     34 import android.system.ErrnoException;
     35 import android.text.TextUtils;
     36 import android.text.format.DateUtils;
     37 import android.util.Slog;
     38 
     39 import com.android.providers.downloads.StorageUtils.ConcreteFile;
     40 
     41 import libcore.io.IoUtils;
     42 
     43 import com.google.android.collect.Lists;
     44 import com.google.android.collect.Sets;
     45 
     46 import java.io.File;
     47 import java.util.ArrayList;
     48 import java.util.HashSet;
     49 
     50 /**
     51  * Idle-time service for {@link DownloadManager}. Reconciles database
     52  * metadata and files on disk, which can become inconsistent when files are
     53  * deleted directly on disk.
     54  */
     55 public class DownloadIdleService extends JobService {
     56     private static final int IDLE_JOB_ID = -100;
     57 
     58     private class IdleRunnable implements Runnable {
     59         private JobParameters mParams;
     60 
     61         public IdleRunnable(JobParameters params) {
     62             mParams = params;
     63         }
     64 
     65         @Override
     66         public void run() {
     67             cleanStale();
     68             cleanOrphans();
     69             jobFinished(mParams, false);
     70         }
     71     }
     72 
     73     @Override
     74     public boolean onStartJob(JobParameters params) {
     75         Helpers.getAsyncHandler().post(new IdleRunnable(params));
     76         return true;
     77     }
     78 
     79     @Override
     80     public boolean onStopJob(JobParameters params) {
     81         // We're okay being killed at any point, so we don't worry about
     82         // checkpointing before tearing down.
     83         return false;
     84     }
     85 
     86     public static void scheduleIdlePass(Context context) {
     87         final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
     88         if (scheduler.getPendingJob(IDLE_JOB_ID) == null) {
     89             final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID,
     90                     new ComponentName(context, DownloadIdleService.class))
     91                             .setPeriodic(12 * DateUtils.HOUR_IN_MILLIS)
     92                             .setRequiresCharging(true)
     93                             .setRequiresDeviceIdle(true)
     94                             .build();
     95             scheduler.schedule(job);
     96         }
     97     }
     98 
     99     private interface StaleQuery {
    100         final String[] PROJECTION = new String[] {
    101                 Downloads.Impl._ID,
    102                 Downloads.Impl.COLUMN_STATUS,
    103                 Downloads.Impl.COLUMN_LAST_MODIFICATION,
    104                 Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI };
    105 
    106         final int _ID = 0;
    107     }
    108 
    109     /**
    110      * Remove stale downloads that third-party apps probably forgot about. We
    111      * only consider non-visible downloads that haven't been touched in over a
    112      * week.
    113      */
    114     public void cleanStale() {
    115         final ContentResolver resolver = getContentResolver();
    116 
    117         final long modifiedBefore = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS;
    118         final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
    119                 StaleQuery.PROJECTION, Downloads.Impl.COLUMN_STATUS + " >= '200' AND "
    120                         + Downloads.Impl.COLUMN_LAST_MODIFICATION + " <= '" + modifiedBefore
    121                         + "' AND " + Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + " == '0'",
    122                 null, null);
    123 
    124         int count = 0;
    125         try {
    126             while (cursor.moveToNext()) {
    127                 final long id = cursor.getLong(StaleQuery._ID);
    128                 resolver.delete(ContentUris.withAppendedId(
    129                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), null, null);
    130                 count++;
    131             }
    132         } finally {
    133             IoUtils.closeQuietly(cursor);
    134         }
    135 
    136         Slog.d(TAG, "Removed " + count + " stale downloads");
    137     }
    138 
    139     private interface OrphanQuery {
    140         final String[] PROJECTION = new String[] {
    141                 Downloads.Impl._ID,
    142                 Downloads.Impl._DATA };
    143 
    144         final int _ID = 0;
    145         final int _DATA = 1;
    146     }
    147 
    148     /**
    149      * Clean up orphan downloads, both in database and on disk.
    150      */
    151     public void cleanOrphans() {
    152         final ContentResolver resolver = getContentResolver();
    153 
    154         // Collect known files from database
    155         final HashSet<ConcreteFile> fromDb = Sets.newHashSet();
    156         final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
    157                 OrphanQuery.PROJECTION, null, null, null);
    158         try {
    159             while (cursor.moveToNext()) {
    160                 final String path = cursor.getString(OrphanQuery._DATA);
    161                 if (TextUtils.isEmpty(path)) continue;
    162 
    163                 final File file = new File(path);
    164                 try {
    165                     fromDb.add(new ConcreteFile(file));
    166                 } catch (ErrnoException e) {
    167                     // File probably no longer exists
    168                     final String state = Environment.getExternalStorageState(file);
    169                     if (Environment.MEDIA_UNKNOWN.equals(state)
    170                             || Environment.MEDIA_MOUNTED.equals(state)) {
    171                         // File appears to live on internal storage, or a
    172                         // currently mounted device, so remove it from database.
    173                         // This logic preserves files on external storage while
    174                         // media is removed.
    175                         final long id = cursor.getLong(OrphanQuery._ID);
    176                         Slog.d(TAG, "Missing " + file + ", deleting " + id);
    177                         resolver.delete(ContentUris.withAppendedId(
    178                                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), null, null);
    179                     }
    180                 }
    181             }
    182         } finally {
    183             IoUtils.closeQuietly(cursor);
    184         }
    185 
    186         // Collect known files from disk
    187         final int uid = android.os.Process.myUid();
    188         final ArrayList<ConcreteFile> fromDisk = Lists.newArrayList();
    189         fromDisk.addAll(listFilesRecursive(getCacheDir(), null, uid));
    190         fromDisk.addAll(listFilesRecursive(getFilesDir(), null, uid));
    191         fromDisk.addAll(listFilesRecursive(Environment.getDownloadCacheDirectory(), null, uid));
    192 
    193         Slog.d(TAG, "Found " + fromDb.size() + " files in database");
    194         Slog.d(TAG, "Found " + fromDisk.size() + " files on disk");
    195 
    196         // Delete files no longer referenced by database
    197         for (ConcreteFile file : fromDisk) {
    198             if (!fromDb.contains(file)) {
    199                 Slog.d(TAG, "Missing db entry, deleting " + file.file);
    200                 file.file.delete();
    201             }
    202         }
    203     }
    204 }
    205