/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.work.impl.utils; import static android.app.AlarmManager.RTC_WAKEUP; import static android.app.PendingIntent.FLAG_NO_CREATE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import android.util.Log; import androidx.work.impl.WorkManagerImpl; import androidx.work.impl.background.systemjob.SystemJobScheduler; import java.util.concurrent.TimeUnit; /** * WorkManager is restarted after an app was force stopped. * Alarms and Jobs get cancelled when an application is force-stopped. To reschedule, we * create a pending alarm that will not survive force stops. * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ForceStopRunnable implements Runnable { private static final String TAG = "ForceStopRunnable"; @VisibleForTesting static final String ACTION_FORCE_STOP_RESCHEDULE = "ACTION_FORCE_STOP_RESCHEDULE"; // All our alarms are use request codes which are > 0. private static final int ALARM_ID = -1; private static final long TEN_YEARS = TimeUnit.DAYS.toMillis(10 * 365); private final Context mContext; private final WorkManagerImpl mWorkManager; public ForceStopRunnable(@NonNull Context context, @NonNull WorkManagerImpl workManager) { mContext = context.getApplicationContext(); mWorkManager = workManager; } @Override public void run() { if (shouldCancelPersistedJobs()) { cancelAllInJobScheduler(); Log.d(TAG, "Migrating persisted jobs."); mWorkManager.rescheduleEligibleWork(); // Mark the jobs as migrated. mWorkManager.getPreferences().setMigratedPersistedJobs(); } else if (isForceStopped()) { Log.d(TAG, "Application was force-stopped, rescheduling."); mWorkManager.rescheduleEligibleWork(); } } /** * @return {@code true} If the application was force stopped. */ @VisibleForTesting public boolean isForceStopped() { // Alarms get cancelled when an app is force-stopped starting at Eclair MR1. // Cancelling of Jobs on force-stop was introduced in N-MR1 (SDK 25). // Even though API 23, 24 are probably safe, OEMs may choose to do // something different. PendingIntent pendingIntent = getPendingIntent(ALARM_ID, FLAG_NO_CREATE); if (pendingIntent == null) { setAlarm(ALARM_ID); return true; } else { return false; } } /** * @return {@code true} If persisted jobs in JobScheduler need to be cancelled. */ @VisibleForTesting public boolean shouldCancelPersistedJobs() { return Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL && mWorkManager.getPreferences().shouldMigratePersistedJobs(); } /** * @param alarmId The stable alarm id to be used. * @param flags The {@link PendingIntent} flags. * @return an instance of the {@link PendingIntent}. */ @VisibleForTesting public PendingIntent getPendingIntent(int alarmId, int flags) { Intent intent = getIntent(); return PendingIntent.getBroadcast(mContext, alarmId, intent, flags); } /** * @return The instance of {@link Intent} used to keep track of force stops. */ @VisibleForTesting public Intent getIntent() { Intent intent = new Intent(); intent.setComponent(new ComponentName(mContext, ForceStopRunnable.BroadcastReceiver.class)); intent.setAction(ACTION_FORCE_STOP_RESCHEDULE); return intent; } /** * Cancels all the persisted jobs in {@link JobScheduler}. */ @VisibleForTesting @TargetApi(WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) public void cancelAllInJobScheduler() { SystemJobScheduler.jobSchedulerCancelAll(mContext); } private void setAlarm(int alarmId) { AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); // Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm. PendingIntent pendingIntent = getPendingIntent(alarmId, FLAG_UPDATE_CURRENT); long triggerAt = System.currentTimeMillis() + TEN_YEARS; if (alarmManager != null) { if (Build.VERSION.SDK_INT >= 19) { alarmManager.setExact(RTC_WAKEUP, triggerAt, pendingIntent); } else { alarmManager.set(RTC_WAKEUP, triggerAt, pendingIntent); } } } /** * A {@link android.content.BroadcastReceiver} which takes care of recreating the * long lived alarm which helps track force stops for an application. * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static class BroadcastReceiver extends android.content.BroadcastReceiver { private static final String TAG = "ForceStopRunnable$Rcvr"; @Override public void onReceive(Context context, Intent intent) { if (intent != null) { String action = intent.getAction(); if (ACTION_FORCE_STOP_RESCHEDULE.equals(action)) { Log.v(TAG, "Rescheduling alarm that keeps track of force-stops."); WorkManagerImpl workManager = WorkManagerImpl.getInstance(); ForceStopRunnable runnable = new ForceStopRunnable(context, workManager); runnable.setAlarm(ForceStopRunnable.ALARM_ID); } } } } }