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