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 package androidx.work.impl.background.systemjob; 17 18 import android.annotation.TargetApi; 19 import android.app.job.JobInfo; 20 import android.app.job.JobScheduler; 21 import android.content.Context; 22 import android.os.Build; 23 import android.os.PersistableBundle; 24 import android.support.annotation.NonNull; 25 import android.support.annotation.RestrictTo; 26 import android.support.annotation.VisibleForTesting; 27 import android.util.Log; 28 29 import androidx.work.impl.Scheduler; 30 import androidx.work.impl.WorkDatabase; 31 import androidx.work.impl.WorkManagerImpl; 32 import androidx.work.impl.model.SystemIdInfo; 33 import androidx.work.impl.model.WorkSpec; 34 import androidx.work.impl.utils.IdGenerator; 35 36 import java.util.List; 37 38 /** 39 * A class that schedules work using {@link android.app.job.JobScheduler}. 40 * 41 * @hide 42 */ 43 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 44 @TargetApi(WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) 45 public class SystemJobScheduler implements Scheduler { 46 47 private static final String TAG = "SystemJobScheduler"; 48 49 private final JobScheduler mJobScheduler; 50 private final WorkManagerImpl mWorkManager; 51 private final IdGenerator mIdGenerator; 52 private final SystemJobInfoConverter mSystemJobInfoConverter; 53 54 public SystemJobScheduler(@NonNull Context context, @NonNull WorkManagerImpl workManager) { 55 this(context, 56 workManager, 57 (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE), 58 new SystemJobInfoConverter(context)); 59 } 60 61 @VisibleForTesting 62 public SystemJobScheduler( 63 Context context, 64 WorkManagerImpl workManager, 65 JobScheduler jobScheduler, 66 SystemJobInfoConverter systemJobInfoConverter) { 67 mWorkManager = workManager; 68 mJobScheduler = jobScheduler; 69 mIdGenerator = new IdGenerator(context); 70 mSystemJobInfoConverter = systemJobInfoConverter; 71 } 72 73 @Override 74 public void schedule(WorkSpec... workSpecs) { 75 WorkDatabase workDatabase = mWorkManager.getWorkDatabase(); 76 77 for (WorkSpec workSpec : workSpecs) { 78 try { 79 workDatabase.beginTransaction(); 80 81 SystemIdInfo info = workDatabase.systemIdInfoDao() 82 .getSystemIdInfo(workSpec.id); 83 84 int jobId = info != null ? info.systemId : mIdGenerator.nextJobSchedulerIdWithRange( 85 mWorkManager.getConfiguration().getMinJobSchedulerID(), 86 mWorkManager.getConfiguration().getMaxJobSchedulerID()); 87 88 if (info == null) { 89 SystemIdInfo newSystemIdInfo = new SystemIdInfo(workSpec.id, jobId); 90 mWorkManager.getWorkDatabase() 91 .systemIdInfoDao() 92 .insertSystemIdInfo(newSystemIdInfo); 93 } 94 95 scheduleInternal(workSpec, jobId); 96 97 // API 23 JobScheduler only kicked off jobs if there were at least two jobs in the 98 // queue, even if the job constraints were met. This behavior was considered 99 // undesirable and later changed in Marshmallow MR1. To match the new behavior, 100 // we will double-schedule jobs on API 23 and de-dupe them 101 // in SystemJobService as needed. 102 if (Build.VERSION.SDK_INT == 23) { 103 int nextJobId = mIdGenerator.nextJobSchedulerIdWithRange( 104 mWorkManager.getConfiguration().getMinJobSchedulerID(), 105 mWorkManager.getConfiguration().getMaxJobSchedulerID()); 106 107 scheduleInternal(workSpec, nextJobId); 108 } 109 110 workDatabase.setTransactionSuccessful(); 111 } finally { 112 workDatabase.endTransaction(); 113 } 114 } 115 } 116 117 /** 118 * Schedules one job with JobScheduler. 119 * 120 * @param workSpec The {@link WorkSpec} to schedule with JobScheduler. 121 */ 122 @VisibleForTesting 123 public void scheduleInternal(WorkSpec workSpec, int jobId) { 124 JobInfo jobInfo = mSystemJobInfoConverter.convert(workSpec, jobId); 125 Log.d(TAG, String.format("Scheduling work ID %s Job ID %s", workSpec.id, jobId)); 126 mJobScheduler.schedule(jobInfo); 127 } 128 129 @Override 130 public void cancel(@NonNull String workSpecId) { 131 // Note: despite what the word "pending" and the associated Javadoc might imply, this is 132 // actually a list of all unfinished jobs that JobScheduler knows about for the current 133 // process. 134 List<JobInfo> allJobInfos = mJobScheduler.getAllPendingJobs(); 135 if (allJobInfos != null) { // Apparently this CAN be null on API 23? 136 for (JobInfo jobInfo : allJobInfos) { 137 if (workSpecId.equals( 138 jobInfo.getExtras().getString(SystemJobInfoConverter.EXTRA_WORK_SPEC_ID))) { 139 140 // Its safe to call this method twice. 141 mWorkManager.getWorkDatabase() 142 .systemIdInfoDao() 143 .removeSystemIdInfo(workSpecId); 144 145 mJobScheduler.cancel(jobInfo.getId()); 146 147 // See comment in #schedule. 148 if (Build.VERSION.SDK_INT != 23) { 149 return; 150 } 151 } 152 } 153 } 154 } 155 156 /** 157 * Cancels all the jobs owned by {@link androidx.work.WorkManager} in {@link JobScheduler}. 158 */ 159 public static void jobSchedulerCancelAll(@NonNull Context context) { 160 JobScheduler jobScheduler = (JobScheduler) 161 context.getSystemService(Context.JOB_SCHEDULER_SERVICE); 162 163 if (jobScheduler != null) { 164 List<JobInfo> jobInfos = jobScheduler.getAllPendingJobs(); 165 // Apparently this can be null on API 23? 166 if (jobInfos != null) { 167 for (JobInfo jobInfo : jobInfos) { 168 PersistableBundle extras = jobInfo.getExtras(); 169 // This is a job scheduled by WorkManager. 170 if (extras.containsKey(SystemJobInfoConverter.EXTRA_WORK_SPEC_ID)) { 171 jobScheduler.cancel(jobInfo.getId()); 172 } 173 } 174 } 175 } 176 } 177 } 178