ddfeba41a32a395e7a25dc7c72c48fec95ddac97
[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.util.Slog;
33
34 import com.android.providers.downloads.StorageUtils.ConcreteFile;
35 import com.google.android.collect.Lists;
36 import com.google.android.collect.Sets;
37
38 import libcore.io.IoUtils;
39
40 import java.io.File;
41 import java.util.ArrayList;
42 import java.util.HashSet;
43
44 /**
45  * Idle-time service for {@link DownloadManager}. Reconciles database
46  * metadata and files on disk, which can become inconsistent when files are
47  * deleted directly on disk.
48  */
49 public class DownloadIdleService extends JobService {
50
51     private class IdleRunnable implements Runnable {
52         private JobParameters mParams;
53
54         public IdleRunnable(JobParameters params) {
55             mParams = params;
56         }
57
58         @Override
59         public void run() {
60             cleanOrphans();
61             jobFinished(mParams, false);
62         }
63     };
64
65     @Override
66     public boolean onStartJob(JobParameters params) {
67         new Thread(new IdleRunnable(params)).start();
68         return true;
69     }
70
71     @Override
72     public boolean onStopJob(JobParameters params) {
73         // We're okay being killed at any point, so we don't worry about
74         // checkpointing before tearing down.
75         return false;
76     }
77
78     private interface DownloadQuery {
79         final String[] PROJECTION = new String[] {
80                 Downloads.Impl._ID,
81                 Downloads.Impl._DATA };
82
83         final int _ID = 0;
84         final int _DATA = 1;
85     }
86
87     /**
88      * Clean up orphan downloads, both in database and on disk.
89      */
90     public void cleanOrphans() {
91         final ContentResolver resolver = getContentResolver();
92
93         // Collect known files from database
94         final HashSet<ConcreteFile> fromDb = Sets.newHashSet();
95         final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
96                 DownloadQuery.PROJECTION, null, null, null);
97         try {
98             while (cursor.moveToNext()) {
99                 final String path = cursor.getString(DownloadQuery._DATA);
100                 if (TextUtils.isEmpty(path)) continue;
101
102                 final File file = new File(path);
103                 try {
104                     fromDb.add(new ConcreteFile(file));
105                 } catch (ErrnoException e) {
106                     // File probably no longer exists
107                     final String state = Environment.getExternalStorageState(file);
108                     if (Environment.MEDIA_UNKNOWN.equals(state)
109                             || Environment.MEDIA_MOUNTED.equals(state)) {
110                         // File appears to live on internal storage, or a
111                         // currently mounted device, so remove it from database.
112                         // This logic preserves files on external storage while
113                         // media is removed.
114                         final long id = cursor.getLong(DownloadQuery._ID);
115                         Slog.d(TAG, "Missing " + file + ", deleting " + id);
116                         resolver.delete(ContentUris.withAppendedId(
117                                 Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), null, null);
118                     }
119                 }
120             }
121         } finally {
122             IoUtils.closeQuietly(cursor);
123         }
124
125         // Collect known files from disk
126         final int uid = android.os.Process.myUid();
127         final ArrayList<ConcreteFile> fromDisk = Lists.newArrayList();
128         fromDisk.addAll(listFilesRecursive(getCacheDir(), null, uid));
129         fromDisk.addAll(listFilesRecursive(getFilesDir(), null, uid));
130         fromDisk.addAll(listFilesRecursive(Environment.getDownloadCacheDirectory(), null, uid));
131
132         Slog.d(TAG, "Found " + fromDb.size() + " files in database");
133         Slog.d(TAG, "Found " + fromDisk.size() + " files on disk");
134
135         // Delete files no longer referenced by database
136         for (ConcreteFile file : fromDisk) {
137             if (!fromDb.contains(file)) {
138                 Slog.d(TAG, "Missing db entry, deleting " + file.file);
139                 file.file.delete();
140             }
141         }
142     }
143 }