1 /*
2  * Copyright 2017 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 androidx.work.impl.background.systemjob;
18 
19 import static android.app.job.JobParameters.STOP_REASON_APP_STANDBY;
20 import static android.app.job.JobParameters.STOP_REASON_BACKGROUND_RESTRICTION;
21 import static android.app.job.JobParameters.STOP_REASON_CANCELLED_BY_APP;
22 import static android.app.job.JobParameters.STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW;
23 import static android.app.job.JobParameters.STOP_REASON_CONSTRAINT_CHARGING;
24 import static android.app.job.JobParameters.STOP_REASON_CONSTRAINT_CONNECTIVITY;
25 import static android.app.job.JobParameters.STOP_REASON_CONSTRAINT_DEVICE_IDLE;
26 import static android.app.job.JobParameters.STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW;
27 import static android.app.job.JobParameters.STOP_REASON_DEVICE_STATE;
28 import static android.app.job.JobParameters.STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED;
29 import static android.app.job.JobParameters.STOP_REASON_PREEMPT;
30 import static android.app.job.JobParameters.STOP_REASON_QUOTA;
31 import static android.app.job.JobParameters.STOP_REASON_SYSTEM_PROCESSING;
32 import static android.app.job.JobParameters.STOP_REASON_TIMEOUT;
33 import static android.app.job.JobParameters.STOP_REASON_UNDEFINED;
34 import static android.app.job.JobParameters.STOP_REASON_USER;
35 
36 import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_GENERATION;
37 import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_ID;
38 
39 import android.app.Application;
40 import android.app.job.JobParameters;
41 import android.app.job.JobScheduler;
42 import android.app.job.JobService;
43 import android.net.Network;
44 import android.net.Uri;
45 import android.os.Build;
46 import android.os.Looper;
47 import android.os.PersistableBundle;
48 
49 import androidx.annotation.MainThread;
50 import androidx.annotation.RequiresApi;
51 import androidx.annotation.RestrictTo;
52 import androidx.work.Logger;
53 import androidx.work.WorkInfo;
54 import androidx.work.WorkerParameters;
55 import androidx.work.impl.ExecutionListener;
56 import androidx.work.impl.Processor;
57 import androidx.work.impl.StartStopToken;
58 import androidx.work.impl.StartStopTokens;
59 import androidx.work.impl.WorkLauncher;
60 import androidx.work.impl.WorkLauncherImpl;
61 import androidx.work.impl.WorkManagerImpl;
62 import androidx.work.impl.model.WorkGenerationalId;
63 
64 import org.jspecify.annotations.NonNull;
65 import org.jspecify.annotations.Nullable;
66 
67 import java.util.Arrays;
68 import java.util.HashMap;
69 import java.util.Map;
70 
71 /**
72  * Service invoked by {@link JobScheduler} to run work tasks.
73  */
74 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
75 @RequiresApi(WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL)
76 public class SystemJobService extends JobService implements ExecutionListener {
77     private static final String TAG = Logger.tagWithPrefix("SystemJobService");
78     private WorkManagerImpl mWorkManagerImpl;
79     private final Map<WorkGenerationalId, JobParameters> mJobParameters = new HashMap<>();
80     private final StartStopTokens mStartStopTokens = StartStopTokens.create(false);
81     private WorkLauncher mWorkLauncher;
82 
83     @Override
onCreate()84     public void onCreate() {
85         super.onCreate();
86         try {
87             mWorkManagerImpl = WorkManagerImpl.getInstance(getApplicationContext());
88             Processor processor = mWorkManagerImpl.getProcessor();
89             mWorkLauncher = new WorkLauncherImpl(processor,
90                     mWorkManagerImpl.getWorkTaskExecutor());
91             processor.addExecutionListener(this);
92         } catch (IllegalStateException e) {
93             // This can occur if...
94             // 1. The app is performing an auto-backup.  Prior to O, JobScheduler could erroneously
95             //    try to send commands to JobService in this state (b/32180780).  Since neither
96             //    Application#onCreate nor ContentProviders have run, WorkManager won't be
97             //    initialized.  In this case, we should ignore all JobScheduler commands and tell it
98             //    to retry.
99             // 2. The app is not performing auto-backup.  WorkManagerInitializer has been disabled
100             //    but WorkManager is not manually initialized in Application#onCreate.  This is a
101             //    developer error and we should throw an Exception.
102             if (!Application.class.equals(getApplication().getClass())) {
103                 // During auto-backup, we don't get a custom Application subclass.  This code path
104                 // indicates we are either performing auto-backup or the user never used a custom
105                 // Application class (or both).
106                 throw new IllegalStateException("WorkManager needs to be initialized via a "
107                         + "ContentProvider#onCreate() or an Application#onCreate().", e);
108             }
109             Logger.get().warning(TAG, "Could not find WorkManager instance; this may be because "
110                     + "an auto-backup is in progress. Ignoring JobScheduler commands for now. "
111                     + "Please make sure that you are initializing WorkManager if you have manually "
112                     + "disabled WorkManagerInitializer.");
113         }
114     }
115 
116     @Override
onDestroy()117     public void onDestroy() {
118         super.onDestroy();
119         if (mWorkManagerImpl != null) {
120             mWorkManagerImpl.getProcessor().removeExecutionListener(this);
121         }
122     }
123 
124     @Override
onStartJob(@onNull JobParameters params)125     public boolean onStartJob(@NonNull JobParameters params) {
126         assertMainThread("onStartJob");
127         if (mWorkManagerImpl == null) {
128             Logger.get().debug(TAG, "WorkManager is not initialized; requesting retry.");
129             jobFinished(params, true);
130             return false;
131         }
132 
133         WorkGenerationalId workGenerationalId = workGenerationalIdFromJobParameters(params);
134         if (workGenerationalId == null) {
135             Logger.get().error(TAG, "WorkSpec id not found!");
136             return false;
137         }
138 
139         if (mJobParameters.containsKey(workGenerationalId)) {
140             // This condition may happen due to our workaround for an undesired behavior in API
141             // 23.  See the documentation in {@link SystemJobScheduler#schedule}.
142             Logger.get().debug(TAG, "Job is already being executed by SystemJobService: "
143                     + workGenerationalId);
144             return false;
145         }
146 
147         // We don't need to worry about the case where JobParams#isOverrideDeadlineExpired()
148         // returns true. This is because JobScheduler ensures that for PeriodicWork, constraints
149         // are actually met irrespective.
150 
151         Logger.get().debug(TAG, "onStartJob for " + workGenerationalId);
152         mJobParameters.put(workGenerationalId, params);
153 
154         WorkerParameters.RuntimeExtras runtimeExtras = null;
155         if (Build.VERSION.SDK_INT >= 24) {
156             runtimeExtras = new WorkerParameters.RuntimeExtras();
157             if (Api24Impl.getTriggeredContentUris(params) != null) {
158                 runtimeExtras.triggeredContentUris =
159                         Arrays.asList(Api24Impl.getTriggeredContentUris(params));
160             }
161             if (Api24Impl.getTriggeredContentAuthorities(params) != null) {
162                 runtimeExtras.triggeredContentAuthorities =
163                         Arrays.asList(Api24Impl.getTriggeredContentAuthorities(params));
164             }
165             if (Build.VERSION.SDK_INT >= 28) {
166                 runtimeExtras.network = Api28Impl.getNetwork(params);
167             }
168         }
169 
170         // It is important that we return true, and hang on this onStartJob() request.
171         // The call to startWork() may no-op because the WorkRequest could have been picked up
172         // by the GreedyScheduler, and was already being executed. GreedyScheduler does not
173         // handle retries, and the Processor notifies all Schedulers about an intent to reschedule.
174         // In such cases, we rely on SystemJobService to ask for a reschedule by calling
175         // jobFinished(params, true) in onExecuted(...);
176         // For more information look at b/123211993
177         mWorkLauncher.startWork(mStartStopTokens.tokenFor(workGenerationalId), runtimeExtras);
178         return true;
179     }
180 
181     @Override
onStopJob(@onNull JobParameters params)182     public boolean onStopJob(@NonNull JobParameters params) {
183         assertMainThread("onStopJob");
184         if (mWorkManagerImpl == null) {
185             Logger.get().debug(TAG, "WorkManager is not initialized; requesting retry.");
186             return true;
187         }
188 
189         WorkGenerationalId workGenerationalId = workGenerationalIdFromJobParameters(params);
190         if (workGenerationalId == null) {
191             Logger.get().error(TAG, "WorkSpec id not found!");
192             return false;
193         }
194 
195         Logger.get().debug(TAG, "onStopJob for " + workGenerationalId);
196 
197         mJobParameters.remove(workGenerationalId);
198         StartStopToken runId = mStartStopTokens.remove(workGenerationalId);
199         if (runId != null) {
200             int stopReason;
201             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
202                 stopReason = Api31Impl.getStopReason(params);
203             } else {
204                 stopReason = WorkInfo.STOP_REASON_UNKNOWN;
205             }
206             //
207             mWorkLauncher.stopWorkWithReason(runId, stopReason);
208         }
209         return !mWorkManagerImpl.getProcessor().isCancelled(workGenerationalId.getWorkSpecId());
210     }
211 
212     @MainThread
213     @Override
onExecuted(@onNull WorkGenerationalId id, boolean needsReschedule)214     public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) {
215         assertMainThread("onExecuted");
216         Logger.get().debug(TAG, id.getWorkSpecId() + " executed on JobScheduler");
217         JobParameters parameters = mJobParameters.remove(id);
218         mStartStopTokens.remove(id);
219         if (parameters != null) {
220             jobFinished(parameters, needsReschedule);
221         }
222     }
223 
224     @SuppressWarnings("ConstantConditions")
workGenerationalIdFromJobParameters( @onNull JobParameters parameters )225     private static @Nullable WorkGenerationalId workGenerationalIdFromJobParameters(
226             @NonNull JobParameters parameters
227     ) {
228         try {
229             PersistableBundle extras = parameters.getExtras();
230             if (extras != null && extras.containsKey(EXTRA_WORK_SPEC_ID)) {
231                 return new WorkGenerationalId(extras.getString(EXTRA_WORK_SPEC_ID),
232                         extras.getInt(EXTRA_WORK_SPEC_GENERATION));
233             }
234         } catch (NullPointerException e) {
235             // b/138441699: BaseBundle.getString sometimes throws an NPE.  Ignore and return null.
236         }
237         return null;
238     }
239 
240     @RequiresApi(24)
241     static class Api24Impl {
Api24Impl()242         private Api24Impl() {
243             // This class is not instantiable.
244         }
245 
getTriggeredContentUris(JobParameters jobParameters)246         static Uri[] getTriggeredContentUris(JobParameters jobParameters) {
247             return jobParameters.getTriggeredContentUris();
248         }
249 
getTriggeredContentAuthorities(JobParameters jobParameters)250         static String[] getTriggeredContentAuthorities(JobParameters jobParameters) {
251             return jobParameters.getTriggeredContentAuthorities();
252         }
253     }
254 
255     @RequiresApi(28)
256     static class Api28Impl {
Api28Impl()257         private Api28Impl() {
258             // This class is not instantiable.
259         }
260 
getNetwork(JobParameters jobParameters)261         static Network getNetwork(JobParameters jobParameters) {
262             return jobParameters.getNetwork();
263         }
264     }
265 
266     @RequiresApi(31)
267     static class Api31Impl {
Api31Impl()268         private Api31Impl() {
269             // This class is not instantiable.
270         }
271 
getStopReason(JobParameters jobParameters)272         static int getStopReason(JobParameters jobParameters) {
273             return stopReason(jobParameters.getStopReason());
274         }
275     }
276 
277     // making sure that we return only values that WorkManager is aware of.
stopReason(int jobReason)278     static int stopReason(int jobReason) {
279         int reason;
280         switch (jobReason) {
281             case STOP_REASON_APP_STANDBY:
282             case STOP_REASON_BACKGROUND_RESTRICTION:
283             case STOP_REASON_CANCELLED_BY_APP:
284             case STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW:
285             case STOP_REASON_CONSTRAINT_CHARGING:
286             case STOP_REASON_CONSTRAINT_CONNECTIVITY:
287             case STOP_REASON_CONSTRAINT_DEVICE_IDLE:
288             case STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW:
289             case STOP_REASON_DEVICE_STATE:
290             case STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED:
291             case STOP_REASON_PREEMPT:
292             case STOP_REASON_QUOTA:
293             case STOP_REASON_SYSTEM_PROCESSING:
294             case STOP_REASON_TIMEOUT:
295             case STOP_REASON_UNDEFINED:
296             case STOP_REASON_USER:
297                 reason = jobReason;
298                 break;
299             default:
300                 reason = WorkInfo.STOP_REASON_UNKNOWN;
301         }
302         return reason;
303     }
304 
assertMainThread(String methodName)305     private static void assertMainThread(String methodName) {
306         if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
307             throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
308                     + " thread");
309         }
310     }
311 }
312