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