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