Trim stale downloads from third-party apps.
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadIdleService.java
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.JobParameters;
24 import android.app.job.JobService;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.database.Cursor;
28 import android.os.Environment;
29 import android.provider.Downloads;
30 import android.system.ErrnoException;
31 import android.text.TextUtils;
32 import android.text.format.DateUtils;
33 import android.util.Slog;
34
35 import com.android.providers.downloads.StorageUtils.ConcreteFile;
36 import com.google.android.collect.Lists;
37 import com.google.android.collect.Sets;
38
39 import libcore.io.IoUtils;
40
41 import java.io.File;
42 import java.util.ArrayList;
43 import 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  */
50 public 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 }