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