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