1 /*
2  * Copyright (C) 2016 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.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
20 
21 import static com.android.providers.downloads.Constants.TAG;
22 
23 import android.app.job.JobParameters;
24 import android.app.job.JobService;
25 import android.database.ContentObserver;
26 import android.util.Log;
27 import android.util.SparseArray;
28 
29 /**
30  * Service that hosts download jobs. Each active download job is handled as a
31  * unique {@link DownloadThread} instance.
32  * <p>
33  * The majority of downloads should have ETag values to enable resuming, so if a
34  * given download isn't able to finish in the normal job timeout (10 minutes),
35  * we just reschedule the job and resume again in the future.
36  */
37 public class DownloadJobService extends JobService {
38     // @GuardedBy("mActiveThreads")
39     private SparseArray<DownloadThread> mActiveThreads = new SparseArray<>();
40 
41     @Override
onCreate()42     public void onCreate() {
43         super.onCreate();
44 
45         // While someone is bound to us, watch for database changes that should
46         // trigger notification updates.
47         getContentResolver().registerContentObserver(ALL_DOWNLOADS_CONTENT_URI, true, mObserver);
48     }
49 
50     @Override
onDestroy()51     public void onDestroy() {
52         super.onDestroy();
53         getContentResolver().unregisterContentObserver(mObserver);
54     }
55 
56     @Override
onStartJob(JobParameters params)57     public boolean onStartJob(JobParameters params) {
58         final int id = params.getJobId();
59 
60         // Spin up thread to handle this download
61         final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id);
62         if (info == null) {
63             Log.w(TAG, "Odd, no details found for download " + id);
64             return false;
65         }
66 
67         final DownloadThread thread;
68         synchronized (mActiveThreads) {
69             if (mActiveThreads.indexOfKey(id) >= 0) {
70                 Log.w(TAG, "Odd, already running download " + id);
71                 return false;
72             }
73             thread = new DownloadThread(this, params, info);
74             mActiveThreads.put(id, thread);
75         }
76         thread.start();
77 
78         return true;
79     }
80 
81     @Override
onStopJob(JobParameters params)82     public boolean onStopJob(JobParameters params) {
83         final int id = params.getJobId();
84         Log.d(TAG, "onStopJob id=" + id + ", reason=" + params.getStopReason());
85 
86         final DownloadThread thread;
87         synchronized (mActiveThreads) {
88             thread = mActiveThreads.removeReturnOld(id);
89         }
90         if (thread != null) {
91             // If the thread is still running, ask it to gracefully shutdown,
92             // and reschedule ourselves to resume in the future.
93             thread.requestShutdown();
94 
95             Helpers.scheduleJob(this, DownloadInfo.queryDownloadInfo(this, id));
96         }
97         return false;
98     }
99 
jobFinishedInternal(JobParameters params, boolean needsReschedule)100     public void jobFinishedInternal(JobParameters params, boolean needsReschedule) {
101         final int id = params.getJobId();
102 
103         synchronized (mActiveThreads) {
104             mActiveThreads.remove(params.getJobId());
105         }
106         if (needsReschedule) {
107             Helpers.scheduleJob(this, DownloadInfo.queryDownloadInfo(this, id));
108         }
109 
110         // Update notifications one last time while job is protecting us
111         mObserver.onChange(false);
112 
113         // We do our own rescheduling above
114         jobFinished(params, false);
115     }
116 
117     private ContentObserver mObserver = new ContentObserver(Helpers.getAsyncHandler()) {
118         @Override
119         public void onChange(boolean selfChange) {
120             Helpers.getDownloadNotifier(DownloadJobService.this).update();
121         }
122     };
123 }
124