bug:3099994 NPE in DownloadManager when deleting non-media file
[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.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.ServiceConnection;
29 import android.database.ContentObserver;
30 import android.database.Cursor;
31 import android.media.IMediaScannerListener;
32 import android.media.IMediaScannerService;
33 import android.net.Uri;
34 import android.os.Environment;
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 com.google.android.collect.Maps;
44 import com.google.common.annotations.VisibleForTesting;
45
46 import java.io.File;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.Iterator;
50 import java.util.Map;
51 import java.util.Set;
52
53
54 /**
55  * Performs the background downloads requested by applications that use the Downloads provider.
56  */
57 public class DownloadService extends Service {
58     /** amount of time to wait to connect to MediaScannerService before timing out */
59     private static final long WAIT_TIMEOUT = 10 * 1000;
60
61     /** Observer to get notified when the content observer's data changes */
62     private DownloadManagerContentObserver mObserver;
63
64     /** Class to handle Notification Manager updates */
65     private DownloadNotification mNotifier;
66
67     /**
68      * The Service's view of the list of downloads, mapping download IDs to the corresponding info
69      * object. This is kept independently from the content provider, and the Service only initiates
70      * downloads based on this data, so that it can deal with situation where the data in the
71      * content provider changes or disappears.
72      */
73     private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
74
75     /**
76      * The thread that updates the internal download list from the content
77      * provider.
78      */
79     @VisibleForTesting
80     UpdateThread mUpdateThread;
81
82     /**
83      * Whether the internal download list should be updated from the content
84      * provider.
85      */
86     private boolean mPendingUpdate;
87
88     /**
89      * The ServiceConnection object that tells us when we're connected to and disconnected from
90      * the Media Scanner
91      */
92     private MediaScannerConnection mMediaScannerConnection;
93
94     private boolean mMediaScannerConnecting;
95
96     /**
97      * The IPC interface to the Media Scanner
98      */
99     private IMediaScannerService mMediaScannerService;
100
101     @VisibleForTesting
102     SystemFacade mSystemFacade;
103
104     /**
105      * Receives notifications when the data in the content provider changes
106      */
107     private class DownloadManagerContentObserver extends ContentObserver {
108
109         public DownloadManagerContentObserver() {
110             super(new Handler());
111         }
112
113         /**
114          * Receives notification when the data in the observed content
115          * provider changes.
116          */
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     public IBinder onBind(Intent i) {
192         throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
193     }
194
195     /**
196      * Initializes the service when it is first created
197      */
198     public void onCreate() {
199         super.onCreate();
200         if (Constants.LOGVV) {
201             Log.v(Constants.TAG, "Service onCreate");
202         }
203
204         if (mSystemFacade == null) {
205             mSystemFacade = new RealSystemFacade(this);
206         }
207
208         mObserver = new DownloadManagerContentObserver();
209         getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
210                 true, mObserver);
211
212         mMediaScannerService = null;
213         mMediaScannerConnecting = false;
214         mMediaScannerConnection = new MediaScannerConnection();
215
216         mNotifier = new DownloadNotification(this, mSystemFacade);
217         mSystemFacade.cancelAllNotifications();
218
219         updateFromProvider();
220     }
221
222     @Override
223     public int onStartCommand(Intent intent, int flags, int startId) {
224         int returnValue = super.onStartCommand(intent, flags, startId);
225         if (Constants.LOGVV) {
226             Log.v(Constants.TAG, "Service onStart");
227         }
228         updateFromProvider();
229         return returnValue;
230     }
231
232     /**
233      * Cleans up when the service is destroyed
234      */
235     public void onDestroy() {
236         getContentResolver().unregisterContentObserver(mObserver);
237         if (Constants.LOGVV) {
238             Log.v(Constants.TAG, "Service onDestroy");
239         }
240         super.onDestroy();
241     }
242
243     /**
244      * Parses data from the content provider into private array
245      */
246     private void updateFromProvider() {
247         synchronized (this) {
248             mPendingUpdate = true;
249             if (mUpdateThread == null) {
250                 mUpdateThread = new UpdateThread();
251                 mSystemFacade.startThread(mUpdateThread);
252             }
253         }
254     }
255
256     private class UpdateThread extends Thread {
257         public UpdateThread() {
258             super("Download Service");
259         }
260
261         public void run() {
262             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
263
264             trimDatabase();
265             removeSpuriousFiles();
266
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
306                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
307                         long id = cursor.getLong(idColumn);
308                         idsNoLongerInDatabase.remove(id);
309                         DownloadInfo info = mDownloads.get(id);
310                         if (info != null) {
311                             updateDownload(reader, info, now);
312                         } else {
313                             info = insertDownload(reader, now);
314                         }
315
316                         if (info.shouldScanFile() && !scanFile(info, true, false)) {
317                             mustScan = true;
318                             keepService = true;
319                         }
320                         if (info.hasCompletionNotification()) {
321                             keepService = true;
322                         }
323                         long next = info.nextAction(now);
324                         if (next == 0) {
325                             keepService = true;
326                         } else if (next > 0 && next < wakeUp) {
327                             wakeUp = next;
328                         }
329                     }
330                 } finally {
331                     cursor.close();
332                 }
333
334                 for (Long id : idsNoLongerInDatabase) {
335                     deleteDownload(id);
336                 }
337
338                 // is there a need to start the DownloadService? yes, if there are rows to be
339                 // deleted.
340                 if (!mustScan) {
341                     for (DownloadInfo info : mDownloads.values()) {
342                         if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
343                             mustScan = true;
344                             keepService = true;
345                             break;
346                         }
347                     }
348                 }
349                 mNotifier.updateNotification(mDownloads.values());
350                 if (mustScan) {
351                     bindMediaScanner();
352                 } else {
353                     mMediaScannerConnection.disconnectMediaScanner();
354                 }
355
356                 // look for all rows with deleted flag set and delete the rows from the database
357                 // permanently
358                 for (DownloadInfo info : mDownloads.values()) {
359                     if (info.mDeleted) {
360                         // this row is to be deleted from the database. but does it have
361                         // mediaProviderUri?
362                         if (TextUtils.isEmpty(info.mMediaProviderUri)) {
363                             if (info.shouldScanFile()) {
364                                 // initiate rescan of the file to - which will populate
365                                 // mediaProviderUri column in this row
366                                 if (!scanFile(info, true, false)) {
367                                     throw new IllegalStateException("scanFile failed!");
368                                 }
369                             } else {
370                                 // this file should NOT be scanned. delete the file.
371                                 Helpers.deleteFile(getContentResolver(), info.mId, info.mFileName,
372                                         info.mMimeType);
373                             }
374                         } else {
375                             // yes it has mediaProviderUri column already filled in.
376                             // delete it from MediaProvider database and then from downloads table
377                             // in DownProvider database (the order of deletion is important).
378                             getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
379                                     null);
380                             getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
381                                     Downloads.Impl._ID + " = ? ",
382                                     new String[]{String.valueOf(info.mId)});
383                         }
384                     }
385                 }
386             }
387         }
388
389         private void bindMediaScanner() {
390             if (!mMediaScannerConnecting) {
391                 Intent intent = new Intent();
392                 intent.setClassName("com.android.providers.media",
393                         "com.android.providers.media.MediaScannerService");
394                 mMediaScannerConnecting = true;
395                 bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
396             }
397         }
398
399         private void scheduleAlarm(long wakeUp) {
400             AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
401             if (alarms == null) {
402                 Log.e(Constants.TAG, "couldn't get alarm manager");
403                 return;
404             }
405
406             if (Constants.LOGV) {
407                 Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
408             }
409
410             Intent intent = new Intent(Constants.ACTION_RETRY);
411             intent.setClassName("com.android.providers.downloads",
412                     DownloadReceiver.class.getName());
413             alarms.set(
414                     AlarmManager.RTC_WAKEUP,
415                     mSystemFacade.currentTimeMillis() + wakeUp,
416                     PendingIntent.getBroadcast(DownloadService.this, 0, intent,
417                             PendingIntent.FLAG_ONE_SHOT));
418         }
419     }
420
421     /**
422      * Removes files that may have been left behind in the cache directory
423      */
424     private void removeSpuriousFiles() {
425         File[] files = Environment.getDownloadCacheDirectory().listFiles();
426         if (files == null) {
427             // The cache folder doesn't appear to exist (this is likely the case
428             // when running the simulator).
429             return;
430         }
431         HashSet<String> fileSet = new HashSet<String>();
432         for (int i = 0; i < files.length; i++) {
433             if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
434                 continue;
435             }
436             if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
437                 continue;
438             }
439             fileSet.add(files[i].getPath());
440         }
441
442         Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
443                 new String[] { Downloads.Impl._DATA }, null, null, null);
444         if (cursor != null) {
445             if (cursor.moveToFirst()) {
446                 do {
447                     fileSet.remove(cursor.getString(0));
448                 } while (cursor.moveToNext());
449             }
450             cursor.close();
451         }
452         Iterator<String> iterator = fileSet.iterator();
453         while (iterator.hasNext()) {
454             String filename = iterator.next();
455             if (Constants.LOGV) {
456                 Log.v(Constants.TAG, "deleting spurious file " + filename);
457             }
458             new File(filename).delete();
459         }
460     }
461
462     /**
463      * Drops old rows from the database to prevent it from growing too large
464      */
465     private void trimDatabase() {
466         Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
467                 new String[] { Downloads.Impl._ID },
468                 Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
469                 Downloads.Impl.COLUMN_LAST_MODIFICATION);
470         if (cursor == null) {
471             // This isn't good - if we can't do basic queries in our database, nothing's gonna work
472             Log.e(Constants.TAG, "null cursor in trimDatabase");
473             return;
474         }
475         if (cursor.moveToFirst()) {
476             int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
477             int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
478             while (numDelete > 0) {
479                 Uri downloadUri = ContentUris.withAppendedId(
480                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
481                 getContentResolver().delete(downloadUri, null, null);
482                 if (!cursor.moveToNext()) {
483                     break;
484                 }
485                 numDelete--;
486             }
487         }
488         cursor.close();
489     }
490
491     /**
492      * Keeps a local copy of the info about a download, and initiates the
493      * download if appropriate.
494      */
495     private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
496         DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
497         mDownloads.put(info.mId, info);
498
499         if (Constants.LOGVV) {
500             info.logVerboseInfo();
501         }
502
503         info.startIfReady(now);
504         return info;
505     }
506
507     /**
508      * Updates the local copy of the info about a download.
509      */
510     private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
511         int oldVisibility = info.mVisibility;
512         int oldStatus = info.mStatus;
513
514         reader.updateFromDatabase(info);
515
516         boolean lostVisibility =
517                 oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
518                 && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
519                 && Downloads.Impl.isStatusCompleted(info.mStatus);
520         boolean justCompleted =
521                 !Downloads.Impl.isStatusCompleted(oldStatus)
522                 && Downloads.Impl.isStatusCompleted(info.mStatus);
523         if (lostVisibility || justCompleted) {
524             mSystemFacade.cancelNotification(info.mId);
525         }
526
527         info.startIfReady(now);
528     }
529
530     /**
531      * Removes the local copy of the info about a download.
532      */
533     private void deleteDownload(long id) {
534         DownloadInfo info = mDownloads.get(id);
535         if (info.shouldScanFile()) {
536             scanFile(info, false, false);
537         }
538         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
539             info.mStatus = Downloads.Impl.STATUS_CANCELED;
540         }
541         if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
542             new File(info.mFileName).delete();
543         }
544         mSystemFacade.cancelNotification(info.mId);
545         mDownloads.remove(info.mId);
546     }
547
548     /**
549      * Attempts to scan the file if necessary.
550      * @return true if the file has been properly scanned.
551      */
552     private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
553             final boolean deleteFile) {
554         synchronized (this) {
555             if (mMediaScannerService == null) {
556                 // not bound to mediaservice. but if in the process of connecting to it, wait until
557                 // connection is resolved
558                 while (mMediaScannerConnecting) {
559                     Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
560                     try {
561                         this.wait(WAIT_TIMEOUT);
562                     } catch (InterruptedException e1) {
563                         throw new IllegalStateException("wait interrupted");
564                     }
565                 }
566             }
567             // do we have mediaservice?
568             if (mMediaScannerService == null) {
569                 // no available MediaService And not even in the process of connecting to it
570                 return false;
571             }
572             if (Constants.LOGV) {
573                 Log.v(Constants.TAG, "Scanning file " + info.mFileName);
574             }
575             try {
576                 final Uri key = info.getAllDownloadsUri();
577                 final String mimeType = info.mMimeType;
578                 final ContentResolver resolver = getContentResolver();
579                 final long id = info.mId;
580                 mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
581                         new IMediaScannerListener.Stub() {
582                             public void scanCompleted(String path, Uri uri) {
583                                 if (uri != null && updateDatabase) {
584                                     // file is scanned and mediaprovider returned uri. store it in downloads
585                                     // table (i.e., update this downloaded file's row)
586                                     ContentValues values = new ContentValues();
587                                     values.put(Constants.MEDIA_SCANNED, 1);
588                                     values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
589                                             uri.toString());
590                                     getContentResolver().update(key, values, null, null);
591                                 } else if (uri == null && deleteFile) {
592                                     // callback returned NO uri..that means this file doesn't
593                                     // exist in MediaProvider. but it still needs to be deleted
594                                     // TODO don't scan files that are not scannable by MediaScanner.
595                                     //      create a public method in MediaFile.java to return false
596                                     //      if the given file's mimetype is not any of the types
597                                     //      the mediaprovider is interested in.
598                                     Helpers.deleteFile(resolver, id, path, mimeType);
599                                 }
600                             }
601                         });
602                 return true;
603             } catch (RemoteException e) {
604                 Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
605                 return false;
606             }
607         }
608     }
609 }