1 /*
2  * Copyright (C) 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 com.android.voicemail.impl.scheduling;
18 
19 import android.annotation.TargetApi;
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.JobScheduler;
23 import android.app.job.JobService;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.SharedPreferences;
27 import android.os.Build.VERSION_CODES;
28 import android.os.Bundle;
29 import android.os.Parcelable;
30 import android.preference.PreferenceManager;
31 import android.support.annotation.MainThread;
32 import com.android.dialer.constants.ScheduledJobIds;
33 import com.android.voicemail.impl.Assert;
34 import com.android.voicemail.impl.VvmLog;
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /** A {@link JobService} that will trigger the background execution of {@link TaskExecutor}. */
39 @TargetApi(VERSION_CODES.O)
40 public class TaskSchedulerJobService extends JobService implements TaskExecutor.Job {
41 
42   private static final String TAG = "TaskSchedulerJobService";
43 
44   private static final String EXTRA_TASK_EXTRAS_ARRAY = "extra_task_extras_array";
45 
46   private static final String EXTRA_JOB_ID = "extra_job_id";
47 
48   private static final String EXPECTED_JOB_ID =
49       "com.android.voicemail.impl.scheduling.TaskSchedulerJobService.EXPECTED_JOB_ID";
50 
51   private static final String NEXT_JOB_ID =
52       "com.android.voicemail.impl.scheduling.TaskSchedulerJobService.NEXT_JOB_ID";
53 
54   private JobParameters jobParameters;
55 
56   @Override
57   @MainThread
onStartJob(JobParameters params)58   public boolean onStartJob(JobParameters params) {
59     int jobId = params.getTransientExtras().getInt(EXTRA_JOB_ID);
60     int expectedJobId =
61         PreferenceManager.getDefaultSharedPreferences(this).getInt(EXPECTED_JOB_ID, 0);
62     if (jobId != expectedJobId) {
63       VvmLog.e(
64           TAG, "Job " + jobId + " is not the last scheduled job " + expectedJobId + ", ignoring");
65       return false; // nothing more to do. Job not running in background.
66     }
67     VvmLog.i(TAG, "starting " + jobId);
68     jobParameters = params;
69     TaskExecutor.createRunningInstance(this);
70     TaskExecutor.getRunningInstance()
71         .onStartJob(
72             this,
73             getBundleList(
74                 jobParameters.getTransientExtras().getParcelableArray(EXTRA_TASK_EXTRAS_ARRAY)));
75     return true /* job still running in background */;
76   }
77 
78   @Override
79   @MainThread
onStopJob(JobParameters params)80   public boolean onStopJob(JobParameters params) {
81     TaskExecutor.getRunningInstance().onStopJob();
82     jobParameters = null;
83     return false /* don't reschedule. TaskExecutor service will post a new job */;
84   }
85 
86   /**
87    * Schedule a job to run the {@code pendingTasks}. If a job is already scheduled it will be
88    * appended to the back of the queue and the job will be rescheduled. A job may only be scheduled
89    * when the {@link TaskExecutor} is not running ({@link TaskExecutor#getRunningInstance()}
90    * returning {@code null})
91    *
92    * @param delayMillis delay before running the job. Must be 0 if{@code isNewJob} is true.
93    * @param isNewJob a new job will be forced to run immediately.
94    */
95   @MainThread
scheduleJob( Context context, List<Bundle> pendingTasks, long delayMillis, boolean isNewJob)96   public static void scheduleJob(
97       Context context, List<Bundle> pendingTasks, long delayMillis, boolean isNewJob) {
98     Assert.isMainThread();
99     JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
100     JobInfo pendingJob = jobScheduler.getPendingJob(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB);
101     VvmLog.i(TAG, "scheduling job with " + pendingTasks.size() + " tasks");
102     if (pendingJob != null) {
103       if (isNewJob) {
104         List<Bundle> existingTasks =
105             getBundleList(
106                 pendingJob.getTransientExtras().getParcelableArray(EXTRA_TASK_EXTRAS_ARRAY));
107         VvmLog.i(TAG, "merging job with " + existingTasks.size() + " existing tasks");
108         TaskQueue queue = new TaskQueue();
109         queue.fromBundles(context, existingTasks);
110         for (Bundle pendingTask : pendingTasks) {
111           queue.add(Tasks.createTask(context, pendingTask));
112         }
113         pendingTasks = queue.toBundles();
114       }
115       VvmLog.i(TAG, "canceling existing job.");
116       jobScheduler.cancel(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB);
117     }
118     Bundle extras = new Bundle();
119     int jobId = createJobId(context);
120     extras.putInt(EXTRA_JOB_ID, jobId);
121     PreferenceManager.getDefaultSharedPreferences(context)
122         .edit()
123         .putInt(EXPECTED_JOB_ID, jobId)
124         .apply();
125 
126     extras.putParcelableArray(
127         EXTRA_TASK_EXTRAS_ARRAY, pendingTasks.toArray(new Bundle[pendingTasks.size()]));
128     JobInfo.Builder builder =
129         new JobInfo.Builder(
130                 ScheduledJobIds.VVM_TASK_SCHEDULER_JOB,
131                 new ComponentName(context, TaskSchedulerJobService.class))
132             .setTransientExtras(extras)
133             .setMinimumLatency(delayMillis)
134             .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
135     if (isNewJob) {
136       Assert.isTrue(delayMillis == 0);
137       builder.setOverrideDeadline(0);
138       VvmLog.i(TAG, "running job instantly.");
139     }
140     jobScheduler.schedule(builder.build());
141     VvmLog.i(TAG, "job " + jobId + " scheduled");
142   }
143 
144   /**
145    * The system will hold a wakelock when {@link #onStartJob(JobParameters)} is called to ensure the
146    * device will not sleep when the job is still running. Finish the job so the system will release
147    * the wakelock
148    */
149   @Override
finishAsync()150   public void finishAsync() {
151     VvmLog.i(TAG, "finishing job");
152     jobFinished(jobParameters, false);
153     jobParameters = null;
154   }
155 
156   @MainThread
157   @Override
isFinished()158   public boolean isFinished() {
159     Assert.isMainThread();
160     return getSystemService(JobScheduler.class)
161             .getPendingJob(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB)
162         == null;
163   }
164 
getBundleList(Parcelable[] parcelables)165   private static List<Bundle> getBundleList(Parcelable[] parcelables) {
166     List<Bundle> result = new ArrayList<>(parcelables.length);
167     for (Parcelable parcelable : parcelables) {
168       result.add((Bundle) parcelable);
169     }
170     return result;
171   }
172 
createJobId(Context context)173   private static int createJobId(Context context) {
174     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
175     int jobId = sharedPreferences.getInt(NEXT_JOB_ID, 0);
176     sharedPreferences.edit().putInt(NEXT_JOB_ID, jobId + 1).apply();
177     return jobId;
178   }
179 }
180