Unregister observer when tearing down service.
[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 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
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 {
124         public DownloadManagerContentObserver() {
125             super(new Handler());
126         }
127
128         @Override
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
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
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
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
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      */
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      */
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      */
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      */
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      */
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      */
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
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
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 }