Improve how the download manager reports paused statuses.
[android/platform/packages/providers/DownloadProvider.git] / src / com / android / providers / downloads / DownloadService.java
1 /*
2  * Copyright (C) 2008 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 android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.app.Service;
22 import android.content.ComponentName;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.database.ContentObserver;
29 import android.database.Cursor;
30 import android.media.IMediaScannerService;
31 import android.net.Uri;
32 import android.os.Environment;
33 import android.os.Handler;
34 import android.os.IBinder;
35 import android.os.Process;
36 import android.os.RemoteException;
37 import android.provider.Downloads;
38 import android.util.Log;
39
40 import com.google.android.collect.Maps;
41 import com.google.common.annotations.VisibleForTesting;
42
43 import java.io.File;
44 import java.util.HashSet;
45 import java.util.Iterator;
46 import java.util.Map;
47 import java.util.Set;
48
49
50 /**
51  * Performs the background downloads requested by applications that use the Downloads provider.
52  */
53 public class DownloadService extends Service {
54     /** Observer to get notified when the content observer's data changes */
55     private DownloadManagerContentObserver mObserver;
56
57     /** Class to handle Notification Manager updates */
58     private DownloadNotification mNotifier;
59
60     /**
61      * The Service's view of the list of downloads, mapping download IDs to the corresponding info
62      * object. This is kept independently from the content provider, and the Service only initiates
63      * downloads based on this data, so that it can deal with situation where the data in the
64      * content provider changes or disappears.
65      */
66     private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
67
68     /**
69      * The thread that updates the internal download list from the content
70      * provider.
71      */
72     @VisibleForTesting
73     UpdateThread mUpdateThread;
74
75     /**
76      * Whether the internal download list should be updated from the content
77      * provider.
78      */
79     private boolean mPendingUpdate;
80
81     /**
82      * The ServiceConnection object that tells us when we're connected to and disconnected from
83      * the Media Scanner
84      */
85     private MediaScannerConnection mMediaScannerConnection;
86
87     private boolean mMediaScannerConnecting;
88
89     /**
90      * The IPC interface to the Media Scanner
91      */
92     private IMediaScannerService mMediaScannerService;
93
94     @VisibleForTesting
95     SystemFacade mSystemFacade;
96
97     /**
98      * Receives notifications when the data in the content provider changes
99      */
100     private class DownloadManagerContentObserver extends ContentObserver {
101
102         public DownloadManagerContentObserver() {
103             super(new Handler());
104         }
105
106         /**
107          * Receives notification when the data in the observed content
108          * provider changes.
109          */
110         public void onChange(final boolean selfChange) {
111             if (Constants.LOGVV) {
112                 Log.v(Constants.TAG, "Service ContentObserver received notification");
113             }
114             updateFromProvider();
115         }
116
117     }
118
119     /**
120      * Gets called back when the connection to the media
121      * scanner is established or lost.
122      */
123     public class MediaScannerConnection implements ServiceConnection {
124         public void onServiceConnected(ComponentName className, IBinder service) {
125             if (Constants.LOGVV) {
126                 Log.v(Constants.TAG, "Connected to Media Scanner");
127             }
128             mMediaScannerConnecting = false;
129             synchronized (DownloadService.this) {
130                 mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
131                 if (mMediaScannerService != null) {
132                     updateFromProvider();
133                 }
134             }
135         }
136
137         public void disconnectMediaScanner() {
138             synchronized (DownloadService.this) {
139                 if (mMediaScannerService != null) {
140                     mMediaScannerService = null;
141                     if (Constants.LOGVV) {
142                         Log.v(Constants.TAG, "Disconnecting from Media Scanner");
143                     }
144                     try {
145                         unbindService(this);
146                     } catch (IllegalArgumentException ex) {
147                         if (Constants.LOGV) {
148                             Log.v(Constants.TAG, "unbindService threw up: " + ex);
149                         }
150                     }
151                 }
152             }
153         }
154
155         public void onServiceDisconnected(ComponentName className) {
156             if (Constants.LOGVV) {
157                 Log.v(Constants.TAG, "Disconnected from Media Scanner");
158             }
159             synchronized (DownloadService.this) {
160                 mMediaScannerService = null;
161             }
162         }
163     }
164
165     /**
166      * Returns an IBinder instance when someone wants to connect to this
167      * service. Binding to this service is not allowed.
168      *
169      * @throws UnsupportedOperationException
170      */
171     public IBinder onBind(Intent i) {
172         throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
173     }
174
175     /**
176      * Initializes the service when it is first created
177      */
178     public void onCreate() {
179         super.onCreate();
180         if (Constants.LOGVV) {
181             Log.v(Constants.TAG, "Service onCreate");
182         }
183
184         if (mSystemFacade == null) {
185             mSystemFacade = new RealSystemFacade(this);
186         }
187
188         mObserver = new DownloadManagerContentObserver();
189         getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
190                 true, mObserver);
191
192         mMediaScannerService = null;
193         mMediaScannerConnecting = false;
194         mMediaScannerConnection = new MediaScannerConnection();
195
196         mNotifier = new DownloadNotification(this, mSystemFacade);
197         mSystemFacade.cancelAllNotifications();
198
199         updateFromProvider();
200     }
201
202     @Override
203     public int onStartCommand(Intent intent, int flags, int startId) {
204         int returnValue = super.onStartCommand(intent, flags, startId);
205         if (Constants.LOGVV) {
206             Log.v(Constants.TAG, "Service onStart");
207         }
208         updateFromProvider();
209         return returnValue;
210     }
211
212     /**
213      * Cleans up when the service is destroyed
214      */
215     public void onDestroy() {
216         getContentResolver().unregisterContentObserver(mObserver);
217         if (Constants.LOGVV) {
218             Log.v(Constants.TAG, "Service onDestroy");
219         }
220         super.onDestroy();
221     }
222
223     /**
224      * Parses data from the content provider into private array
225      */
226     private void updateFromProvider() {
227         synchronized (this) {
228             mPendingUpdate = true;
229             if (mUpdateThread == null) {
230                 mUpdateThread = new UpdateThread();
231                 mSystemFacade.startThread(mUpdateThread);
232             }
233         }
234     }
235
236     private class UpdateThread extends Thread {
237         public UpdateThread() {
238             super("Download Service");
239         }
240
241         public void run() {
242             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
243
244             trimDatabase();
245             removeSpuriousFiles();
246
247             boolean keepService = false;
248             // for each update from the database, remember which download is
249             // supposed to get restarted soonest in the future
250             long wakeUp = Long.MAX_VALUE;
251             for (;;) {
252                 synchronized (DownloadService.this) {
253                     if (mUpdateThread != this) {
254                         throw new IllegalStateException(
255                                 "multiple UpdateThreads in DownloadService");
256                     }
257                     if (!mPendingUpdate) {
258                         mUpdateThread = null;
259                         if (!keepService) {
260                             stopSelf();
261                         }
262                         if (wakeUp != Long.MAX_VALUE) {
263                             scheduleAlarm(wakeUp);
264                         }
265                         return;
266                     }
267                     mPendingUpdate = false;
268                 }
269
270                 long now = mSystemFacade.currentTimeMillis();
271                 boolean mustScan = false;
272                 keepService = false;
273                 wakeUp = Long.MAX_VALUE;
274                 Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
275
276                 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
277                         null, null, null, null);
278                 if (cursor == null) {
279                     continue;
280                 }
281                 try {
282                     DownloadInfo.Reader reader =
283                             new DownloadInfo.Reader(getContentResolver(), cursor);
284                     int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
285
286                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
287                         long id = cursor.getLong(idColumn);
288                         idsNoLongerInDatabase.remove(id);
289                         DownloadInfo info = mDownloads.get(id);
290                         if (info != null) {
291                             updateDownload(reader, info, now);
292                         } else {
293                             info = insertDownload(reader, now);
294                         }
295
296                         if (info.shouldScanFile() && !scanFile(info, true)) {
297                             mustScan = true;
298                             keepService = true;
299                         }
300                         if (info.hasCompletionNotification()) {
301                             keepService = true;
302                         }
303                         long next = info.nextAction(now);
304                         if (next == 0) {
305                             keepService = true;
306                         } else if (next > 0 && next < wakeUp) {
307                             wakeUp = next;
308                         }
309                     }
310                 } finally {
311                     cursor.close();
312                 }
313
314                 for (Long id : idsNoLongerInDatabase) {
315                     deleteDownload(id);
316                 }
317
318                 mNotifier.updateNotification(mDownloads.values());
319
320                 if (mustScan) {
321                     bindMediaScanner();
322                 } else {
323                     mMediaScannerConnection.disconnectMediaScanner();
324                 }
325             }
326         }
327
328         private void bindMediaScanner() {
329             if (!mMediaScannerConnecting) {
330                 Intent intent = new Intent();
331                 intent.setClassName("com.android.providers.media",
332                         "com.android.providers.media.MediaScannerService");
333                 mMediaScannerConnecting = true;
334                 bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
335             }
336         }
337
338         private void scheduleAlarm(long wakeUp) {
339             AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
340             if (alarms == null) {
341                 Log.e(Constants.TAG, "couldn't get alarm manager");
342                 return;
343             }
344
345             if (Constants.LOGV) {
346                 Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
347             }
348
349             Intent intent = new Intent(Constants.ACTION_RETRY);
350             intent.setClassName("com.android.providers.downloads",
351                     DownloadReceiver.class.getName());
352             alarms.set(
353                     AlarmManager.RTC_WAKEUP,
354                     mSystemFacade.currentTimeMillis() + wakeUp,
355                     PendingIntent.getBroadcast(DownloadService.this, 0, intent,
356                             PendingIntent.FLAG_ONE_SHOT));
357         }
358     }
359
360     /**
361      * Removes files that may have been left behind in the cache directory
362      */
363     private void removeSpuriousFiles() {
364         File[] files = Environment.getDownloadCacheDirectory().listFiles();
365         if (files == null) {
366             // The cache folder doesn't appear to exist (this is likely the case
367             // when running the simulator).
368             return;
369         }
370         HashSet<String> fileSet = new HashSet<String>();
371         for (int i = 0; i < files.length; i++) {
372             if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
373                 continue;
374             }
375             if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
376                 continue;
377             }
378             fileSet.add(files[i].getPath());
379         }
380
381         Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
382                 new String[] { Downloads.Impl._DATA }, null, null, null);
383         if (cursor != null) {
384             if (cursor.moveToFirst()) {
385                 do {
386                     fileSet.remove(cursor.getString(0));
387                 } while (cursor.moveToNext());
388             }
389             cursor.close();
390         }
391         Iterator<String> iterator = fileSet.iterator();
392         while (iterator.hasNext()) {
393             String filename = iterator.next();
394             if (Constants.LOGV) {
395                 Log.v(Constants.TAG, "deleting spurious file " + filename);
396             }
397             new File(filename).delete();
398         }
399     }
400
401     /**
402      * Drops old rows from the database to prevent it from growing too large
403      */
404     private void trimDatabase() {
405         Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
406                 new String[] { Downloads.Impl._ID },
407                 Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
408                 Downloads.Impl.COLUMN_LAST_MODIFICATION);
409         if (cursor == null) {
410             // This isn't good - if we can't do basic queries in our database, nothing's gonna work
411             Log.e(Constants.TAG, "null cursor in trimDatabase");
412             return;
413         }
414         if (cursor.moveToFirst()) {
415             int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
416             int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
417             while (numDelete > 0) {
418                 Uri downloadUri = ContentUris.withAppendedId(
419                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
420                 getContentResolver().delete(downloadUri, null, null);
421                 if (!cursor.moveToNext()) {
422                     break;
423                 }
424                 numDelete--;
425             }
426         }
427         cursor.close();
428     }
429
430     /**
431      * Keeps a local copy of the info about a download, and initiates the
432      * download if appropriate.
433      */
434     private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
435         DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
436         mDownloads.put(info.mId, info);
437
438         if (Constants.LOGVV) {
439             info.logVerboseInfo();
440         }
441
442         info.startIfReady(now);
443         return info;
444     }
445
446     /**
447      * Updates the local copy of the info about a download.
448      */
449     private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
450         int oldVisibility = info.mVisibility;
451         int oldStatus = info.mStatus;
452
453         reader.updateFromDatabase(info);
454
455         boolean lostVisibility =
456                 oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
457                 && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
458                 && Downloads.Impl.isStatusCompleted(info.mStatus);
459         boolean justCompleted =
460                 !Downloads.Impl.isStatusCompleted(oldStatus)
461                 && Downloads.Impl.isStatusCompleted(info.mStatus);
462         if (lostVisibility || justCompleted) {
463             mSystemFacade.cancelNotification(info.mId);
464         }
465
466         info.startIfReady(now);
467     }
468
469     /**
470      * Removes the local copy of the info about a download.
471      */
472     private void deleteDownload(long id) {
473         DownloadInfo info = mDownloads.get(id);
474         if (info.shouldScanFile()) {
475             scanFile(info, false);
476         }
477         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
478             info.mStatus = Downloads.Impl.STATUS_CANCELED;
479         }
480         if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
481             new File(info.mFileName).delete();
482         }
483         mSystemFacade.cancelNotification(info.mId);
484         mDownloads.remove(info.mId);
485     }
486
487     /**
488      * Attempts to scan the file if necessary.
489      * @return true if the file has been properly scanned.
490      */
491     private boolean scanFile(DownloadInfo info, boolean updateDatabase) {
492         synchronized (this) {
493             if (mMediaScannerService == null) {
494                 return false;
495             }
496             try {
497                 if (Constants.LOGV) {
498                     Log.v(Constants.TAG, "Scanning file " + info.mFileName);
499                 }
500                 mMediaScannerService.scanFile(info.mFileName, info.mMimeType);
501                 if (updateDatabase) {
502                     ContentValues values = new ContentValues();
503                     values.put(Constants.MEDIA_SCANNED, 1);
504                     getContentResolver().update(info.getAllDownloadsUri(), values, null, null);
505                 }
506                 return true;
507             } catch (RemoteException e) {
508                 Log.d(Constants.TAG, "Failed to scan file " + info.mFileName);
509                 return false;
510             }
511         }
512     }
513
514 }