• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
IdleRunnable(JobParameters params)61         public IdleRunnable(JobParameters params) {
62             mParams = params;
63         }
64 
65         @Override
run()66         public void run() {
67             cleanStale();
68             cleanOrphans();
69             jobFinished(mParams, false);
70         }
71     }
72 
73     @Override
onStartJob(JobParameters params)74     public boolean onStartJob(JobParameters params) {
75         Helpers.getAsyncHandler().post(new IdleRunnable(params));
76         return true;
77     }
78 
79     @Override
onStopJob(JobParameters params)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 
scheduleIdlePass(Context context)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      */
cleanStale()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      */
cleanOrphans()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