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.utils; 18 19 import static android.app.AlarmManager.RTC_WAKEUP; 20 import static android.app.PendingIntent.FLAG_NO_CREATE; 21 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 22 23 import android.annotation.TargetApi; 24 import android.app.AlarmManager; 25 import android.app.PendingIntent; 26 import android.app.job.JobScheduler; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.os.Build; 31 import android.support.annotation.NonNull; 32 import android.support.annotation.RestrictTo; 33 import android.support.annotation.VisibleForTesting; 34 import android.util.Log; 35 36 import androidx.work.impl.WorkManagerImpl; 37 import androidx.work.impl.background.systemjob.SystemJobScheduler; 38 39 import java.util.concurrent.TimeUnit; 40 41 /** 42 * WorkManager is restarted after an app was force stopped. 43 * Alarms and Jobs get cancelled when an application is force-stopped. To reschedule, we 44 * create a pending alarm that will not survive force stops. 45 * 46 * @hide 47 */ 48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 49 public class ForceStopRunnable implements Runnable { 50 51 private static final String TAG = "ForceStopRunnable"; 52 53 @VisibleForTesting 54 static final String ACTION_FORCE_STOP_RESCHEDULE = "ACTION_FORCE_STOP_RESCHEDULE"; 55 56 // All our alarms are use request codes which are > 0. 57 private static final int ALARM_ID = -1; 58 private static final long TEN_YEARS = TimeUnit.DAYS.toMillis(10 * 365); 59 60 private final Context mContext; 61 private final WorkManagerImpl mWorkManager; 62 ForceStopRunnable(@onNull Context context, @NonNull WorkManagerImpl workManager)63 public ForceStopRunnable(@NonNull Context context, @NonNull WorkManagerImpl workManager) { 64 mContext = context.getApplicationContext(); 65 mWorkManager = workManager; 66 } 67 68 @Override run()69 public void run() { 70 if (shouldCancelPersistedJobs()) { 71 cancelAllInJobScheduler(); 72 Log.d(TAG, "Migrating persisted jobs."); 73 mWorkManager.rescheduleEligibleWork(); 74 // Mark the jobs as migrated. 75 mWorkManager.getPreferences().setMigratedPersistedJobs(); 76 } else if (isForceStopped()) { 77 Log.d(TAG, "Application was force-stopped, rescheduling."); 78 mWorkManager.rescheduleEligibleWork(); 79 } 80 } 81 82 /** 83 * @return {@code true} If the application was force stopped. 84 */ 85 @VisibleForTesting isForceStopped()86 public boolean isForceStopped() { 87 // Alarms get cancelled when an app is force-stopped starting at Eclair MR1. 88 // Cancelling of Jobs on force-stop was introduced in N-MR1 (SDK 25). 89 // Even though API 23, 24 are probably safe, OEMs may choose to do 90 // something different. 91 PendingIntent pendingIntent = getPendingIntent(ALARM_ID, FLAG_NO_CREATE); 92 if (pendingIntent == null) { 93 setAlarm(ALARM_ID); 94 return true; 95 } else { 96 return false; 97 } 98 } 99 100 /** 101 * @return {@code true} If persisted jobs in JobScheduler need to be cancelled. 102 */ 103 @VisibleForTesting shouldCancelPersistedJobs()104 public boolean shouldCancelPersistedJobs() { 105 return Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL 106 && mWorkManager.getPreferences().shouldMigratePersistedJobs(); 107 } 108 109 /** 110 * @param alarmId The stable alarm id to be used. 111 * @param flags The {@link PendingIntent} flags. 112 * @return an instance of the {@link PendingIntent}. 113 */ 114 @VisibleForTesting getPendingIntent(int alarmId, int flags)115 public PendingIntent getPendingIntent(int alarmId, int flags) { 116 Intent intent = getIntent(); 117 return PendingIntent.getBroadcast(mContext, alarmId, intent, flags); 118 } 119 120 /** 121 * @return The instance of {@link Intent} used to keep track of force stops. 122 */ 123 @VisibleForTesting getIntent()124 public Intent getIntent() { 125 Intent intent = new Intent(); 126 intent.setComponent(new ComponentName(mContext, ForceStopRunnable.BroadcastReceiver.class)); 127 intent.setAction(ACTION_FORCE_STOP_RESCHEDULE); 128 return intent; 129 } 130 131 /** 132 * Cancels all the persisted jobs in {@link JobScheduler}. 133 */ 134 @VisibleForTesting 135 @TargetApi(WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) cancelAllInJobScheduler()136 public void cancelAllInJobScheduler() { 137 SystemJobScheduler.jobSchedulerCancelAll(mContext); 138 } 139 setAlarm(int alarmId)140 private void setAlarm(int alarmId) { 141 AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 142 // Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm. 143 PendingIntent pendingIntent = getPendingIntent(alarmId, FLAG_UPDATE_CURRENT); 144 long triggerAt = System.currentTimeMillis() + TEN_YEARS; 145 if (alarmManager != null) { 146 if (Build.VERSION.SDK_INT >= 19) { 147 alarmManager.setExact(RTC_WAKEUP, triggerAt, pendingIntent); 148 } else { 149 alarmManager.set(RTC_WAKEUP, triggerAt, pendingIntent); 150 } 151 } 152 } 153 154 /** 155 * A {@link android.content.BroadcastReceiver} which takes care of recreating the 156 * long lived alarm which helps track force stops for an application. 157 * 158 * @hide 159 */ 160 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 161 public static class BroadcastReceiver extends android.content.BroadcastReceiver { 162 private static final String TAG = "ForceStopRunnable$Rcvr"; 163 164 @Override onReceive(Context context, Intent intent)165 public void onReceive(Context context, Intent intent) { 166 if (intent != null) { 167 String action = intent.getAction(); 168 if (ACTION_FORCE_STOP_RESCHEDULE.equals(action)) { 169 Log.v(TAG, "Rescheduling alarm that keeps track of force-stops."); 170 WorkManagerImpl workManager = WorkManagerImpl.getInstance(); 171 ForceStopRunnable runnable = new ForceStopRunnable(context, workManager); 172 runnable.setAlarm(ForceStopRunnable.ALARM_ID); 173 } 174 } 175 } 176 } 177 } 178