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