• 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 android.text.format.DateUtils.MINUTE_IN_MILLIS;
20 import static com.android.providers.downloads.Constants.TAG;
21 
22 import android.app.AlarmManager;
23 import android.app.DownloadManager;
24 import android.app.PendingIntent;
25 import android.app.Service;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.res.Resources;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.Handler;
34 import android.os.HandlerThread;
35 import android.os.IBinder;
36 import android.os.Message;
37 import android.os.Process;
38 import android.provider.Downloads;
39 import android.text.TextUtils;
40 import android.util.Log;
41 
42 import com.android.internal.annotations.GuardedBy;
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 import com.google.common.collect.Sets;
48 
49 import java.io.File;
50 import java.io.FileDescriptor;
51 import java.io.PrintWriter;
52 import java.util.Arrays;
53 import java.util.Collections;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Set;
57 import java.util.concurrent.ExecutorService;
58 import java.util.concurrent.LinkedBlockingQueue;
59 import java.util.concurrent.ThreadPoolExecutor;
60 import java.util.concurrent.TimeUnit;
61 
62 /**
63  * Performs background downloads as requested by applications that use
64  * {@link DownloadManager}. Multiple start commands can be issued at this
65  * service, and it will continue running until no downloads are being actively
66  * processed. It may schedule alarms to resume downloads in future.
67  * <p>
68  * Any database updates important enough to initiate tasks should always be
69  * delivered through {@link Context#startService(Intent)}.
70  */
71 public class DownloadService extends Service {
72     // TODO: migrate WakeLock from individual DownloadThreads out into
73     // DownloadReceiver to protect our entire workflow.
74 
75     private static final boolean DEBUG_LIFECYCLE = false;
76 
77     @VisibleForTesting
78     SystemFacade mSystemFacade;
79 
80     private AlarmManager mAlarmManager;
81     private StorageManager mStorageManager;
82 
83     /** Observer to get notified when the content observer's data changes */
84     private DownloadManagerContentObserver mObserver;
85 
86     /** Class to handle Notification Manager updates */
87     private DownloadNotifier mNotifier;
88 
89     /**
90      * The Service's view of the list of downloads, mapping download IDs to the corresponding info
91      * object. This is kept independently from the content provider, and the Service only initiates
92      * downloads based on this data, so that it can deal with situation where the data in the
93      * content provider changes or disappears.
94      */
95     @GuardedBy("mDownloads")
96     private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
97 
98     private final ExecutorService mExecutor = buildDownloadExecutor();
99 
buildDownloadExecutor()100     private static ExecutorService buildDownloadExecutor() {
101         final int maxConcurrent = Resources.getSystem().getInteger(
102                 com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
103 
104         // Create a bounded thread pool for executing downloads; it creates
105         // threads as needed (up to maximum) and reclaims them when finished.
106         final ThreadPoolExecutor executor = new ThreadPoolExecutor(
107                 maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS,
108                 new LinkedBlockingQueue<Runnable>());
109         executor.allowCoreThreadTimeOut(true);
110         return executor;
111     }
112 
113     private DownloadScanner mScanner;
114 
115     private HandlerThread mUpdateThread;
116     private Handler mUpdateHandler;
117 
118     private volatile int mLastStartId;
119 
120     /**
121      * Receives notifications when the data in the content provider changes
122      */
123     private class DownloadManagerContentObserver extends ContentObserver {
DownloadManagerContentObserver()124         public DownloadManagerContentObserver() {
125             super(new Handler());
126         }
127 
128         @Override
onChange(final boolean selfChange)129         public void onChange(final boolean selfChange) {
130             enqueueUpdate();
131         }
132     }
133 
134     /**
135      * Returns an IBinder instance when someone wants to connect to this
136      * service. Binding to this service is not allowed.
137      *
138      * @throws UnsupportedOperationException
139      */
140     @Override
onBind(Intent i)141     public IBinder onBind(Intent i) {
142         throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
143     }
144 
145     /**
146      * Initializes the service when it is first created
147      */
148     @Override
onCreate()149     public void onCreate() {
150         super.onCreate();
151         if (Constants.LOGVV) {
152             Log.v(Constants.TAG, "Service onCreate");
153         }
154 
155         if (mSystemFacade == null) {
156             mSystemFacade = new RealSystemFacade(this);
157         }
158 
159         mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
160         mStorageManager = new StorageManager(this);
161 
162         mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
163         mUpdateThread.start();
164         mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
165 
166         mScanner = new DownloadScanner(this);
167 
168         mNotifier = new DownloadNotifier(this);
169         mNotifier.cancelAll();
170 
171         mObserver = new DownloadManagerContentObserver();
172         getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
173                 true, mObserver);
174     }
175 
176     @Override
onStartCommand(Intent intent, int flags, int startId)177     public int onStartCommand(Intent intent, int flags, int startId) {
178         int returnValue = super.onStartCommand(intent, flags, startId);
179         if (Constants.LOGVV) {
180             Log.v(Constants.TAG, "Service onStart");
181         }
182         mLastStartId = startId;
183         enqueueUpdate();
184         return returnValue;
185     }
186 
187     @Override
onDestroy()188     public void onDestroy() {
189         getContentResolver().unregisterContentObserver(mObserver);
190         mScanner.shutdown();
191         mUpdateThread.quit();
192         if (Constants.LOGVV) {
193             Log.v(Constants.TAG, "Service onDestroy");
194         }
195         super.onDestroy();
196     }
197 
198     /**
199      * Enqueue an {@link #updateLocked()} pass to occur in future.
200      */
enqueueUpdate()201     private void enqueueUpdate() {
202         mUpdateHandler.removeMessages(MSG_UPDATE);
203         mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
204     }
205 
206     /**
207      * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to
208      * catch any finished operations that didn't trigger an update pass.
209      */
enqueueFinalUpdate()210     private void enqueueFinalUpdate() {
211         mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
212         mUpdateHandler.sendMessageDelayed(
213                 mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
214                 5 * MINUTE_IN_MILLIS);
215     }
216 
217     private static final int MSG_UPDATE = 1;
218     private static final int MSG_FINAL_UPDATE = 2;
219 
220     private Handler.Callback mUpdateCallback = new Handler.Callback() {
221         @Override
222         public boolean handleMessage(Message msg) {
223             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
224 
225             final int startId = msg.arg1;
226             if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
227 
228             // Since database is current source of truth, our "active" status
229             // depends on database state. We always get one final update pass
230             // once the real actions have finished and persisted their state.
231 
232             // TODO: switch to asking real tasks to derive active state
233             // TODO: handle media scanner timeouts
234 
235             final boolean isActive;
236             synchronized (mDownloads) {
237                 isActive = updateLocked();
238             }
239 
240             if (msg.what == MSG_FINAL_UPDATE) {
241                 // Dump thread stacks belonging to pool
242                 for (Map.Entry<Thread, StackTraceElement[]> entry :
243                         Thread.getAllStackTraces().entrySet()) {
244                     if (entry.getKey().getName().startsWith("pool")) {
245                         Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue()));
246                     }
247                 }
248 
249                 // Dump speed and update details
250                 mNotifier.dumpSpeeds();
251 
252                 Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
253                         + "; someone didn't update correctly.");
254             }
255 
256             if (isActive) {
257                 // Still doing useful work, keep service alive. These active
258                 // tasks will trigger another update pass when they're finished.
259 
260                 // Enqueue delayed update pass to catch finished operations that
261                 // didn't trigger an update pass; these are bugs.
262                 enqueueFinalUpdate();
263 
264             } else {
265                 // No active tasks, and any pending update messages can be
266                 // ignored, since any updates important enough to initiate tasks
267                 // will always be delivered with a new startId.
268 
269                 if (stopSelfResult(startId)) {
270                     if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
271                     getContentResolver().unregisterContentObserver(mObserver);
272                     mScanner.shutdown();
273                     mUpdateThread.quit();
274                 }
275             }
276 
277             return true;
278         }
279     };
280 
281     /**
282      * Update {@link #mDownloads} to match {@link DownloadProvider} state.
283      * Depending on current download state it may enqueue {@link DownloadThread}
284      * instances, request {@link DownloadScanner} scans, update user-visible
285      * notifications, and/or schedule future actions with {@link AlarmManager}.
286      * <p>
287      * Should only be called from {@link #mUpdateThread} as after being
288      * requested through {@link #enqueueUpdate()}.
289      *
290      * @return If there are active tasks being processed, as of the database
291      *         snapshot taken in this update.
292      */
updateLocked()293     private boolean updateLocked() {
294         final long now = mSystemFacade.currentTimeMillis();
295 
296         boolean isActive = false;
297         long nextActionMillis = Long.MAX_VALUE;
298 
299         final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
300 
301         final ContentResolver resolver = getContentResolver();
302         final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
303                 null, null, null, null);
304         try {
305             final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
306             final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
307             while (cursor.moveToNext()) {
308                 final long id = cursor.getLong(idColumn);
309                 staleIds.remove(id);
310 
311                 DownloadInfo info = mDownloads.get(id);
312                 if (info != null) {
313                     updateDownload(reader, info, now);
314                 } else {
315                     info = insertDownloadLocked(reader, now);
316                 }
317 
318                 if (info.mDeleted) {
319                     // Delete download if requested, but only after cleaning up
320                     if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
321                         resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
322                     }
323 
324                     deleteFileIfExists(info.mFileName);
325                     resolver.delete(info.getAllDownloadsUri(), null, null);
326 
327                 } else {
328                     // Kick off download task if ready
329                     final boolean activeDownload = info.startDownloadIfReady(mExecutor);
330 
331                     // Kick off media scan if completed
332                     final boolean activeScan = info.startScanIfReady(mScanner);
333 
334                     if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
335                         Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
336                                 + ", activeScan=" + activeScan);
337                     }
338 
339                     isActive |= activeDownload;
340                     isActive |= activeScan;
341                 }
342 
343                 // Keep track of nearest next action
344                 nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
345             }
346         } finally {
347             cursor.close();
348         }
349 
350         // Clean up stale downloads that disappeared
351         for (Long id : staleIds) {
352             deleteDownloadLocked(id);
353         }
354 
355         // Update notifications visible to user
356         mNotifier.updateWith(mDownloads.values());
357 
358         // Set alarm when next action is in future. It's okay if the service
359         // continues to run in meantime, since it will kick off an update pass.
360         if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
361             if (Constants.LOGV) {
362                 Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
363             }
364 
365             final Intent intent = new Intent(Constants.ACTION_RETRY);
366             intent.setClass(this, DownloadReceiver.class);
367             mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
368                     PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
369         }
370 
371         return isActive;
372     }
373 
374     /**
375      * Keeps a local copy of the info about a download, and initiates the
376      * download if appropriate.
377      */
insertDownloadLocked(DownloadInfo.Reader reader, long now)378     private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
379         final DownloadInfo info = reader.newDownloadInfo(
380                 this, mSystemFacade, mStorageManager, mNotifier);
381         mDownloads.put(info.mId, info);
382 
383         if (Constants.LOGVV) {
384             Log.v(Constants.TAG, "processing inserted download " + info.mId);
385         }
386 
387         return info;
388     }
389 
390     /**
391      * Updates the local copy of the info about a download.
392      */
updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now)393     private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
394         reader.updateFromDatabase(info);
395         if (Constants.LOGVV) {
396             Log.v(Constants.TAG, "processing updated download " + info.mId +
397                     ", status: " + info.mStatus);
398         }
399     }
400 
401     /**
402      * Removes the local copy of the info about a download.
403      */
deleteDownloadLocked(long id)404     private void deleteDownloadLocked(long id) {
405         DownloadInfo info = mDownloads.get(id);
406         if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
407             info.mStatus = Downloads.Impl.STATUS_CANCELED;
408         }
409         if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
410             if (Constants.LOGVV) {
411                 Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
412             }
413             deleteFileIfExists(info.mFileName);
414         }
415         mDownloads.remove(info.mId);
416     }
417 
deleteFileIfExists(String path)418     private void deleteFileIfExists(String path) {
419         if (!TextUtils.isEmpty(path)) {
420             if (Constants.LOGVV) {
421                 Log.d(TAG, "deleteFileIfExists() deleting " + path);
422             }
423             final File file = new File(path);
424             if (file.exists() && !file.delete()) {
425                 Log.w(TAG, "file: '" + path + "' couldn't be deleted");
426             }
427         }
428     }
429 
430     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)431     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
432         final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
433         synchronized (mDownloads) {
434             final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
435             Collections.sort(ids);
436             for (Long id : ids) {
437                 final DownloadInfo info = mDownloads.get(id);
438                 info.dump(pw);
439             }
440         }
441     }
442 }
443