Home | History | Annotate | Download | only in downloads
      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 }
    500