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.JobInfo; 24import android.app.job.JobParameters; 25import android.app.job.JobScheduler; 26import android.app.job.JobService; 27import android.content.ComponentName; 28import android.content.ContentResolver; 29import android.content.ContentUris; 30import android.content.Context; 31import android.database.Cursor; 32import android.os.Environment; 33import android.provider.Downloads; 34import android.system.ErrnoException; 35import android.text.TextUtils; 36import android.text.format.DateUtils; 37import android.util.Slog; 38 39import com.android.providers.downloads.StorageUtils.ConcreteFile; 40 41import libcore.io.IoUtils; 42 43import com.google.android.collect.Lists; 44import com.google.android.collect.Sets; 45 46import java.io.File; 47import java.util.ArrayList; 48import 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 */ 55public 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