1 /*
2  * Copyright 2018 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;
18 
19 import static androidx.work.impl.Scheduler.MAX_GREEDY_SCHEDULER_LIMIT;
20 import static androidx.work.impl.WorkManagerImpl.CONTENT_URI_TRIGGER_API_LEVEL;
21 import static androidx.work.impl.utils.PackageManagerHelper.setComponentEnabled;
22 
23 import android.content.Context;
24 import android.os.Build;
25 
26 import androidx.annotation.RestrictTo;
27 import androidx.work.Clock;
28 import androidx.work.Configuration;
29 import androidx.work.Logger;
30 import androidx.work.impl.background.systemalarm.SystemAlarmScheduler;
31 import androidx.work.impl.background.systemalarm.SystemAlarmService;
32 import androidx.work.impl.background.systemjob.SystemJobScheduler;
33 import androidx.work.impl.background.systemjob.SystemJobService;
34 import androidx.work.impl.model.WorkSpec;
35 import androidx.work.impl.model.WorkSpecDao;
36 
37 import org.jspecify.annotations.NonNull;
38 import org.jspecify.annotations.Nullable;
39 
40 import java.util.List;
41 import java.util.concurrent.Executor;
42 
43 /**
44  * Helper methods for {@link Scheduler}s.
45  *
46  * Helps schedule {@link androidx.work.impl.model.WorkSpec}s while enforcing
47  * {@link Scheduler#MAX_SCHEDULER_LIMIT}s.
48  *
49  */
50 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
51 public class Schedulers {
52 
53     public static final String GCM_SCHEDULER = "androidx.work.impl.background.gcm.GcmScheduler";
54     private static final String TAG = Logger.tagWithPrefix("Schedulers");
55 
56     /**
57      * Make sure that once worker has run its dependants are run.
58      */
registerRescheduling( @onNull List<Scheduler> schedulers, @NonNull Processor processor, @NonNull Executor executor, @NonNull WorkDatabase workDatabase, @NonNull Configuration configuration)59     public static void registerRescheduling(
60             @NonNull List<Scheduler> schedulers,
61             @NonNull Processor processor,
62             @NonNull Executor executor,
63             @NonNull WorkDatabase workDatabase,
64             @NonNull Configuration configuration) {
65         processor.addExecutionListener((id, needsReschedule) -> {
66             executor.execute(() -> {
67                 // Try to schedule any newly-unblocked workers, and workers requiring rescheduling
68                 // (such as periodic work using AlarmManager). This code runs after runWorker()
69                 // because it should happen in its own transaction.
70 
71                 // Cancel this work in other schedulers. For example, if this work was
72                 // handled by GreedyScheduler, we should make sure JobScheduler is informed
73                 // that it should remove this job and AlarmManager should remove all related alarms.
74                 for (Scheduler scheduler : schedulers) {
75                     scheduler.cancel(id.getWorkSpecId());
76                 }
77                 Schedulers.schedule(configuration, workDatabase, schedulers);
78             });
79         });
80     }
81 
82     /**
83      * Schedules {@link WorkSpec}s while honoring the {@link Scheduler#MAX_SCHEDULER_LIMIT}.
84      *
85      * @param workDatabase The {@link WorkDatabase}.
86      * @param schedulers   The {@link List} of {@link Scheduler}s to delegate to.
87      */
schedule( @onNull Configuration configuration, @NonNull WorkDatabase workDatabase, @Nullable List<Scheduler> schedulers)88     public static void schedule(
89             @NonNull Configuration configuration,
90             @NonNull WorkDatabase workDatabase,
91             @Nullable List<Scheduler> schedulers) {
92         if (schedulers == null || schedulers.size() == 0) {
93             return;
94         }
95 
96         WorkSpecDao workSpecDao = workDatabase.workSpecDao();
97         List<WorkSpec> eligibleWorkSpecsForLimitedSlots;
98         List<WorkSpec> allEligibleWorkSpecs;
99 
100         workDatabase.beginTransaction();
101         try {
102             List<WorkSpec> contentUriWorkSpecs = null;
103             if (Build.VERSION.SDK_INT >= CONTENT_URI_TRIGGER_API_LEVEL) {
104                 contentUriWorkSpecs = workSpecDao.getEligibleWorkForSchedulingWithContentUris();
105                 markScheduled(workSpecDao, configuration.getClock(), contentUriWorkSpecs);
106             }
107 
108             // Enqueued workSpecs when scheduling limits are applicable.
109             eligibleWorkSpecsForLimitedSlots = workSpecDao.getEligibleWorkForScheduling(
110                     configuration.getMaxSchedulerLimit());
111             markScheduled(workSpecDao, configuration.getClock(), eligibleWorkSpecsForLimitedSlots);
112             if (contentUriWorkSpecs != null) {
113                 eligibleWorkSpecsForLimitedSlots.addAll(contentUriWorkSpecs);
114             }
115 
116             // Enqueued workSpecs when scheduling limits are NOT applicable.
117             allEligibleWorkSpecs = workSpecDao.getAllEligibleWorkSpecsForScheduling(
118                     MAX_GREEDY_SCHEDULER_LIMIT);
119             workDatabase.setTransactionSuccessful();
120         } finally {
121             workDatabase.endTransaction();
122         }
123 
124         if (eligibleWorkSpecsForLimitedSlots.size() > 0) {
125 
126             WorkSpec[] eligibleWorkSpecsArray =
127                     new WorkSpec[eligibleWorkSpecsForLimitedSlots.size()];
128             eligibleWorkSpecsArray =
129                     eligibleWorkSpecsForLimitedSlots.toArray(eligibleWorkSpecsArray);
130 
131             // Delegate to the underlying schedulers.
132             for (Scheduler scheduler : schedulers) {
133                 if (scheduler.hasLimitedSchedulingSlots()) {
134                     scheduler.schedule(eligibleWorkSpecsArray);
135                 }
136             }
137         }
138 
139         if (allEligibleWorkSpecs.size() > 0) {
140             WorkSpec[] enqueuedWorkSpecsArray = new WorkSpec[allEligibleWorkSpecs.size()];
141             enqueuedWorkSpecsArray = allEligibleWorkSpecs.toArray(enqueuedWorkSpecsArray);
142             // Delegate to the underlying schedulers.
143             for (Scheduler scheduler : schedulers) {
144                 if (!scheduler.hasLimitedSchedulingSlots()) {
145                     scheduler.schedule(enqueuedWorkSpecsArray);
146                 }
147             }
148         }
149     }
150 
createBestAvailableBackgroundScheduler(@onNull Context context, @NonNull WorkDatabase workDatabase, Configuration configuration)151     static @NonNull Scheduler createBestAvailableBackgroundScheduler(@NonNull Context context,
152             @NonNull WorkDatabase workDatabase, Configuration configuration) {
153 
154         Scheduler scheduler;
155 
156         if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
157             scheduler = new SystemJobScheduler(context, workDatabase, configuration);
158             setComponentEnabled(context, SystemJobService.class, true);
159             Logger.get().debug(TAG, "Created SystemJobScheduler and enabled SystemJobService");
160         } else {
161             scheduler = tryCreateGcmBasedScheduler(context, configuration.getClock());
162             if (scheduler == null) {
163                 scheduler = new SystemAlarmScheduler(context);
164                 setComponentEnabled(context, SystemAlarmService.class, true);
165                 Logger.get().debug(TAG, "Created SystemAlarmScheduler");
166             }
167         }
168         return scheduler;
169     }
170 
tryCreateGcmBasedScheduler(@onNull Context context, Clock clock)171     private static @Nullable Scheduler tryCreateGcmBasedScheduler(@NonNull Context context,
172             Clock clock) {
173         try {
174             Class<?> klass = Class.forName(GCM_SCHEDULER);
175             Scheduler scheduler =
176                     (Scheduler) klass.getConstructor(Context.class, Clock.class)
177                             .newInstance(context, clock);
178             Logger.get().debug(TAG, "Created " + GCM_SCHEDULER);
179             return scheduler;
180         } catch (Throwable throwable) {
181             Logger.get().debug(TAG, "Unable to create GCM Scheduler", throwable);
182             return null;
183         }
184     }
185 
Schedulers()186     private Schedulers() {
187     }
188 
markScheduled(WorkSpecDao dao, Clock clock, List<WorkSpec> workSpecs)189     private static void markScheduled(WorkSpecDao dao, Clock clock, List<WorkSpec> workSpecs) {
190         if (workSpecs.size() > 0) {
191             long now = clock.currentTimeMillis();
192 
193             // Mark all the WorkSpecs as scheduled.
194             // Calls to Scheduler#schedule() could potentially result in more schedules
195             // on a separate thread. Therefore, this needs to be done first.
196             for (WorkSpec workSpec : workSpecs) {
197                 dao.markWorkSpecScheduled(workSpec.id, now);
198             }
199         }
200     }
201 }
202