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 
SystemJobScheduler(@onNull Context context, @NonNull WorkManagerImpl workManager)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
SystemJobScheduler( Context context, WorkManagerImpl workManager, JobScheduler jobScheduler, SystemJobInfoConverter systemJobInfoConverter)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
schedule(WorkSpec... workSpecs)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
scheduleInternal(WorkSpec workSpec, int jobId)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
cancel(@onNull String workSpecId)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      */
jobSchedulerCancelAll(@onNull Context context)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