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 */
16package androidx.work.impl.background.systemjob;
17
18import android.annotation.TargetApi;
19import android.app.job.JobInfo;
20import android.app.job.JobScheduler;
21import android.content.Context;
22import android.os.Build;
23import android.os.PersistableBundle;
24import android.support.annotation.NonNull;
25import android.support.annotation.RestrictTo;
26import android.support.annotation.VisibleForTesting;
27import android.util.Log;
28
29import androidx.work.impl.Scheduler;
30import androidx.work.impl.WorkDatabase;
31import androidx.work.impl.WorkManagerImpl;
32import androidx.work.impl.model.SystemIdInfo;
33import androidx.work.impl.model.WorkSpec;
34import androidx.work.impl.utils.IdGenerator;
35
36import 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)
45public 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