log when DownloadManager deletes files
[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 com.google.android.collect.Maps;
20 import com.google.common.annotations.VisibleForTesting;
21
22 import android.app.AlarmManager;
23 import android.app.PendingIntent;
24 import android.app.Service;
25 import android.content.ComponentName;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.ServiceConnection;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.media.IMediaScannerListener;
33 import android.media.IMediaScannerService;
34 import android.net.Uri;
35 import android.os.Handler;
36 import android.os.IBinder;
37 import android.os.Process;
38 import android.os.RemoteException;
39 import android.provider.Downloads;
40 import android.text.TextUtils;
41 import android.util.Log;
42
43 import java.io.File;
44 import java.io.FileDescriptor;
45 import java.io.PrintWriter;
46 import java.util.HashSet;
47 import java.util.Map;
48 import java.util.Set;
49
50
51 /**
52  * Performs the background downloads requested by applications that use the Downloads provider.
53  */
54 public class DownloadService extends Service {
55     /** amount of time to wait to connect to MediaScannerService before timing out */
56     private static final long WAIT_TIMEOUT = 10 * 1000;
57
58     /** Observer to get notified when the content observer's data changes */
59     private DownloadManagerContentObserver mObserver;
60
61     /** Class to handle Notification Manager updates */
62     private DownloadNotification mNotifier;
63
64     /**
65      * The Service's view of the list of downloads, mapping download IDs to the corresponding info
66      * object. This is kept independently from the content provider, and the Service only initiates
67      * downloads based on this data, so that it can deal with situation where the data in the
68      * content provider changes or disappears.
69      */
70     private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
71
72     /**
73      * The thread that updates the internal download list from the content
74      * provider.
75      */
76     @VisibleForTesting
77     UpdateThread mUpdateThread;
78
79     /**
80      * Whether the internal download list should be updated from the content
81      * provider.
82      */
83     private boolean mPendingUpdate;
84
85     /**
86      * The ServiceConnection object that tells us when we're connected to and disconnected from
87      * the Media Scanner
88      */
89     private MediaScannerConnection mMediaScannerConnection;
90
91     private boolean mMediaScannerConnecting;
92
93     /**
94      * The IPC interface to the Media Scanner
95      */
96     private IMediaScannerService mMediaScannerService;
97
98     @VisibleForTesting
99     SystemFacade mSystemFacade;
100
101     private StorageManager mStorageManager;
102
103     /**
104      * Receives notifications when the data in the content provider changes
105      */
106     private class DownloadManagerContentObserver extends ContentObserver {
107
108         public DownloadManagerContentObserver() {
109             super(new Handler());
110         }
111
112         /**
113          * Receives notification when the data in the observed content
114          * provider changes.
115          */
116         @Override
117         public void onChange(final boolean selfChange) {
118             if (Constants.LOGVV) {
119                 Log.v(Constants.TAG, "Service ContentObserver received notification");
120             }
121             updateFromProvider();
122         }
123
124     }
125
126     /**
127      * Gets called back when the connection to the media
128      * scanner is established or lost.
129      */
130     public class MediaScannerConnection implements ServiceConnection {
131         public void onServiceConnected(ComponentName className, IBinder service) {
132             if (Constants.LOGVV) {
133                 Log.v(Constants.TAG, "Connected to Media Scanner");
134             }
135             synchronized (DownloadService.this) {
136                 try {
137                     mMediaScannerConnecting = false;
138                     mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
139                     if (mMediaScannerService != null) {
140                         updateFromProvider();
141                     }
142                 } finally {
143                     // notify anyone waiting on successful connection to MediaService
144                     DownloadService.this.notifyAll();
145                 }
146             }
147         }
148
149         public void disconnectMediaScanner() {
150             synchronized (DownloadService.this) {
151                 mMediaScannerConnecting = false;
152                 if (mMediaScannerService != null) {
153                     mMediaScannerService = null;
154                     if (Constants.LOGVV) {
155                         Log.v(Constants.TAG, "Disconnecting from Media Scanner");
156                     }
157                     try {
158                         unbindService(this);
159                     } catch (IllegalArgumentException ex) {
160                         Log.w(Constants.TAG, "unbindService failed: " + ex);
161                     } finally {
162                         // notify anyone waiting on unsuccessful connection to MediaService
163                         DownloadService.this.notifyAll();
164                     }
165                 }
166             }
167         }
168
169         public void onServiceDisconnected(ComponentName className) {
170             try {
171                 if (Constants.LOGVV) {
172                     Log.v(Constants.TAG, "Disconnected from Media Scanner");
173                 }
174             } finally {
175                 synchronized (DownloadService.this) {
176                     mMediaScannerService = null;
177                     mMediaScannerConnecting = false;
178                     // notify anyone waiting on disconnect from MediaService
179                     DownloadService.this.notifyAll();
180                 }
181             }
182         }
183     }
184
185     /**
186      * Returns an IBinder instance when someone wants to connect to this
187      * service. Binding to this service is not allowed.
188      *
189      * @throws UnsupportedOperationException
190      */
191     @Override
192     public IBinder onBind(Intent i) {
193         throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
194     }
195
196     /**
197      * Initializes the service when it is first created
198      */
199     @Override
200     public void onCreate() {
201         super.onCreate();
202         if (Constants.LOGVV) {
203             Log.v(Constants.TAG, "Service onCreate");
204         }
205
206         if (mSystemFacade == null) {
207             mSystemFacade = new RealSystemFacade(this);
208         }
209
210         mObserver = new DownloadManagerContentObserver();
211         getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
212                 true, mObserver);
213
214         mMediaScannerService = null;
215         mMediaScannerConnecting = false;
216         mMediaScannerConnection = new MediaScannerConnection();
217
218         mNotifier = new DownloadNotification(this, mSystemFacade);
219         mSystemFacade.cancelAllNotifications();
220         mStorageManager = StorageManager.getInstance(getApplicationContext());
221         updateFromProvider();
222     }
223
224     @Override
225     public int onStartCommand(Intent intent, int flags, int startId) {
226         int returnValue = super.onStartCommand(intent, flags, startId);
227         if (Constants.LOGVV) {
228             Log.v(Constants.TAG, "Service onStart");
229         }
230         updateFromProvider();
231         return returnValue;
232     }
233
234     /**
235      * Cleans up when the service is destroyed
236      */
237     @Override
238     public void onDestroy() {
239         getContentResolver().unregisterContentObserver(mObserver);
240         if (Constants.LOGVV) {
241             Log.v(Constants.TAG, "Service onDestroy");
242         }
243         super.onDestroy();
244     }
245
246     /**
247      * Parses data from the content provider into private array
248      */
249     private void updateFromProvider() {
250         synchronized (this) {
251             mPendingUpdate = true;
252             if (mUpdateThread == null) {
253                 mUpdateThread = new UpdateThread();
254                 mSystemFacade.startThread(mUpdateThread);
255             }
256         }
257     }
258
259     private class UpdateThread extends Thread {
260         public UpdateThread() {
261             super("Download Service");
262         }
263
264         @Override
265         public void run() {
266             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
267             boolean keepService = false;
268             // for each update from the database, remember which download is
269             // supposed to get restarted soonest in the future
270             long wakeUp = Long.MAX_VALUE;
271             for (;;) {
272                 synchronized (DownloadService.this) {
273                     if (mUpdateThread != this) {
274                         throw new IllegalStateException(
275                                 "multiple UpdateThreads in DownloadService");
276                     }
277                     if (!mPendingUpdate) {
278                         mUpdateThread = null;
279                         if (!keepService) {
280                             stopSelf();
281                         }
282                         if (wakeUp != Long.MAX_VALUE) {
283                             scheduleAlarm(wakeUp);
284                         }
285                         return;
286                     }
287                     mPendingUpdate = false;
288                 }
289
290                 long now = mSystemFacade.currentTimeMillis();
291                 boolean mustScan = false;
292                 keepService = false;
293                 wakeUp = Long.MAX_VALUE;
294                 Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
295
296                 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
297                         null, null, null, null);
298                 if (cursor == null) {
299                     continue;
300                 }
301                 try {
302                     DownloadInfo.Reader reader =
303                             new DownloadInfo.Reader(getContentResolver(), cursor);
304                     int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
305                     if (Constants.LOGVV) {
306                         Log.i(Constants.TAG, "number of rows from downloads-db: " +
307                                 cursor.getCount());
308                     }
309                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
310                         long id = cursor.getLong(idColumn);
311                         idsNoLongerInDatabase.remove(id);
312                         DownloadInfo info = mDownloads.get(id);
313                         if (info != null) {
314                             updateDownload(reader, info, now);
315                         } else {
316                             info = insertDownload(reader, now);
317                         }
318
319                         if (info.shouldScanFile() && !scanFile(info, true, false)) {
320                             mustScan = true;
321                             keepService = true;
322                         }
323                         if (info.hasCompletionNotification()) {
324                             keepService = true;
325                         }
326                         long next = info.nextAction(now);
327                         if (next == 0) {
328                             keepService = true;
329                         } else if (next > 0 && next < wakeUp) {
330                             wakeUp = next;
331                         }
332                     }
333                 } finally {
334                     cursor.close();
335                 }
336
337                 for (Long id : idsNoLongerInDatabase) {
338                     deleteDownload(id);
339                 }
340
341                 // is there a need to start the DownloadService? yes, if there are rows to be
342                 // deleted.
343                 if (!mustScan) {
344                     for (DownloadInfo info : mDownloads.values()) {
345                         if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
346                             mustScan = true;
347                             keepService = true;
348                             break;
349                         }
350                     }
351                 }
352                 mNotifier.updateNotification(mDownloads.values());
353                 if (mustScan) {
354                     bindMediaScanner();
355                 } else {
356                     mMediaScannerConnection.disconnectMediaScanner();
357                 }
358
359                 // look for all rows with deleted flag set and delete the rows from the database
360                 // permanently
361                 for (DownloadInfo info : mDownloads.values()) {
362                     if (info.mDeleted) {
363                         // this row is to be deleted from the database. but does it have
364                         // mediaProviderUri?
365                         if (TextUtils.isEmpty(info.mMediaProviderUri)) {
366                             if (info.shouldScanFile()) {
367                                 // initiate rescan of the file to - which will populate
368                                 // mediaProviderUri column in this row
369                                 if (!scanFile(info, false, true)) {
370                                     throw new IllegalStateException("scanFile failed!");
371                                 }
372                                 continue;
373                             }
374                         } else {
375                             // yes it has mediaProviderUri column already filled in.
376                             // delete it from MediaProvider database.
377                             getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
378                                     null);
379                         }
380                         // delete the file
381                         deleteFileIfExists(info.mFileName);
382                         // delete from the downloads db
383                         getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
384                                 Downloads.Impl._ID + " = ? ",
385                                 new String[]{String.valueOf(info.mId)});
386                     }
387                 }
388             }
389         }
390
391         private void bindMediaScanner() {
392             if (!mMediaScannerConnecting) {
393                 Intent intent = new Intent();
394                 intent.setClassName("com.android.providers.media",
395                         "com.android.providers.media.MediaScannerService");
396                 mMediaScannerConnecting = true;
397                 bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
398             }
399         }
400
401         private void scheduleAlarm(long wakeUp) {
402             AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
403             if (alarms == null) {
404                 Log.e(Constants.TAG, "couldn't get alarm manager");
405                 return;
406             }
407
408             if (Constants.LOGV) {
409                 Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
410             }
411
412             Intent intent = new Intent(Constants.ACTION_RETRY);
413             intent.setClassName("com.android.providers.downloads",
414                     DownloadReceiver.class.getName());
415             alarms.set(
416                     AlarmManager.RTC_WAKEUP,
417                     mSystemFacade.currentTimeMillis() + wakeUp,
418                     PendingIntent.getBroadcast(DownloadService.this, 0, intent,
419                             PendingIntent.FLAG_ONE_SHOT));
420         }
421     }
422
423     /**
424      * Keeps a local copy of the info about a download, and initiates the
425      * download if appropriate.
426      */
427     private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
428         DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
429         mDownloads.put(info.mId, info);
430
431         if (Constants.LOGVV) {
432             Log.v(Constants.TAG, "processing inserted download " + info.mId);
433         }
434
435         info.startIfReady(now, mStorageManager);
436         return info;
437     }
438
439     /**
440      * Updates the local copy of the info about a download.
441      */
442     private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
443         int oldVisibility = info.mVisibility;
444         int oldStatus = info.mStatus;
445
446         reader.updateFromDatabase(info);
447         if (Constants.LOGVV) {
448             Log.v(Constants.TAG, "processing updated download " + info.mId +
449                     ", status: " + info.mStatus);
450         }
451
452         boolean lostVisibility =
453                 oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
454                 && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
455                 && Downloads.Impl.isStatusCompleted(info.mStatus);
456         boolean justCompleted =
457                 !Downloads.Impl.isStatusCompleted(oldStatus)
458                 && Downloads.Impl.isStatusCompleted(info.mStatus);
459         if (lostVisibility || justCompleted) {
460             mSystemFacade.cancelNotification(info.mId);
461         }
462
463         info.startIfReady(now, mStorageManager);
464     }
465
466     /**
467      * Removes the local copy of the info about a download.
468      */
469     private void deleteDownload(long id) {
470         DownloadInfo info = mDownloads.get(id);
471         if (info.shouldScanFile()) {
472             scanFile(info, false, false);
473         }
474         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
475             info.mStatus = Downloads.Impl.STATUS_CANCELED;
476         }
477         if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
478             new File(info.mFileName).delete();
479         }
480         mSystemFacade.cancelNotification(info.mId);
481         mDownloads.remove(info.mId);
482     }
483
484     /**
485      * Attempts to scan the file if necessary.
486      * @return true if the file has been properly scanned.
487      */
488     private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
489             final boolean deleteFile) {
490         synchronized (this) {
491             if (mMediaScannerService == null) {
492                 // not bound to mediaservice. but if in the process of connecting to it, wait until
493                 // connection is resolved
494                 while (mMediaScannerConnecting) {
495                     Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
496                     try {
497                         this.wait(WAIT_TIMEOUT);
498                     } catch (InterruptedException e1) {
499                         throw new IllegalStateException("wait interrupted");
500                     }
501                 }
502             }
503             // do we have mediaservice?
504             if (mMediaScannerService == null) {
505                 // no available MediaService And not even in the process of connecting to it
506                 return false;
507             }
508             if (Constants.LOGV) {
509                 Log.v(Constants.TAG, "Scanning file " + info.mFileName);
510             }
511             try {
512                 final Uri key = info.getAllDownloadsUri();
513                 final long id = info.mId;
514                 mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
515                         new IMediaScannerListener.Stub() {
516                             public void scanCompleted(String path, Uri uri) {
517                                 if (updateDatabase) {
518                                     // Mark this as 'scanned' in the database
519                                     // so that it is NOT subject to re-scanning by MediaScanner
520                                     // next time this database row row is encountered
521                                     ContentValues values = new ContentValues();
522                                     values.put(Constants.MEDIA_SCANNED, 1);
523                                     if (uri != null) {
524                                         values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
525                                                 uri.toString());
526                                     }
527                                     getContentResolver().update(key, values, null, null);
528                                 } else if (deleteFile) {
529                                     if (uri != null) {
530                                         // use the Uri returned to delete it from the MediaProvider
531                                         getContentResolver().delete(uri, null, null);
532                                     }
533                                     // delete the file and delete its row from the downloads db
534                                     deleteFileIfExists(path);
535                                     getContentResolver().delete(
536                                             Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
537                                             Downloads.Impl._ID + " = ? ",
538                                             new String[]{String.valueOf(id)});
539                                 }
540                             }
541                         });
542                 return true;
543             } catch (RemoteException e) {
544                 Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
545                 return false;
546             }
547         }
548     }
549
550     private void deleteFileIfExists(String path) {
551         try {
552             if (!TextUtils.isEmpty(path)) {
553                 Log.i(Constants.TAG, "deleting " + path);
554                 File file = new File(path);
555                 file.delete();
556             }
557         } catch (Exception e) {
558             Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
559         }
560     }
561
562     @Override
563     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
564         for (DownloadInfo info : mDownloads.values()) {
565             info.dump(writer);
566         }
567     }
568 }