Home | History | Annotate | Download | only in utils
      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 
     63     public ForceStopRunnable(@NonNull Context context, @NonNull WorkManagerImpl workManager) {
     64         mContext = context.getApplicationContext();
     65         mWorkManager = workManager;
     66     }
     67 
     68     @Override
     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
     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
    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
    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
    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)
    136     public void cancelAllInJobScheduler() {
    137         SystemJobScheduler.jobSchedulerCancelAll(mContext);
    138     }
    139 
    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
    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