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