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