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.ApplicationExitInfo.REASON_USER_REQUESTED;
21 import static android.app.PendingIntent.FLAG_MUTABLE;
22 import static android.app.PendingIntent.FLAG_NO_CREATE;
23 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
24 
25 import static androidx.work.WorkInfo.State.ENQUEUED;
26 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
27 
28 import android.app.ActivityManager;
29 import android.app.AlarmManager;
30 import android.app.ApplicationExitInfo;
31 import android.app.PendingIntent;
32 import android.content.ComponentName;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.database.sqlite.SQLiteAccessPermException;
36 import android.database.sqlite.SQLiteCantOpenDatabaseException;
37 import android.database.sqlite.SQLiteConstraintException;
38 import android.database.sqlite.SQLiteDatabaseCorruptException;
39 import android.database.sqlite.SQLiteDatabaseLockedException;
40 import android.database.sqlite.SQLiteDiskIOException;
41 import android.database.sqlite.SQLiteException;
42 import android.database.sqlite.SQLiteFullException;
43 import android.database.sqlite.SQLiteTableLockedException;
44 import android.os.Build;
45 import android.text.TextUtils;
46 
47 import androidx.annotation.RestrictTo;
48 import androidx.annotation.VisibleForTesting;
49 import androidx.core.os.UserManagerCompat;
50 import androidx.core.util.Consumer;
51 import androidx.work.Configuration;
52 import androidx.work.Logger;
53 import androidx.work.WorkInfo;
54 import androidx.work.impl.Schedulers;
55 import androidx.work.impl.WorkDatabase;
56 import androidx.work.impl.WorkDatabasePathHelper;
57 import androidx.work.impl.WorkManagerImpl;
58 import androidx.work.impl.background.systemjob.SystemJobScheduler;
59 import androidx.work.impl.model.WorkProgressDao;
60 import androidx.work.impl.model.WorkSpec;
61 import androidx.work.impl.model.WorkSpecDao;
62 
63 import org.jspecify.annotations.NonNull;
64 import org.jspecify.annotations.Nullable;
65 
66 import java.util.List;
67 import java.util.concurrent.TimeUnit;
68 
69 /**
70  * WorkManager is restarted after an app was force stopped.
71  * Alarms and Jobs get cancelled when an application is force-stopped. To reschedule, we
72  * create a pending alarm that will not survive force stops.
73  *
74  */
75 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
76 public class ForceStopRunnable implements Runnable {
77 
78     private static final String TAG = Logger.tagWithPrefix("ForceStopRunnable");
79 
80     @VisibleForTesting
81     static final String ACTION_FORCE_STOP_RESCHEDULE = "ACTION_FORCE_STOP_RESCHEDULE";
82     @VisibleForTesting
83     static final int MAX_ATTEMPTS = 3;
84 
85     // All our alarms are use request codes which are > 0.
86     private static final int ALARM_ID = -1;
87     private static final long BACKOFF_DURATION_MS = 300L;
88     private static final long TEN_YEARS = TimeUnit.DAYS.toMillis(10 * 365);
89 
90     private final Context mContext;
91     private final WorkManagerImpl mWorkManager;
92     private final PreferenceUtils mPreferenceUtils;
93     private int mRetryCount;
94 
ForceStopRunnable(@onNull Context context, @NonNull WorkManagerImpl workManager)95     public ForceStopRunnable(@NonNull Context context, @NonNull WorkManagerImpl workManager) {
96         mContext = context.getApplicationContext();
97         mWorkManager = workManager;
98         mPreferenceUtils = workManager.getPreferenceUtils();
99         mRetryCount = 0;
100     }
101 
102     @Override
run()103     public void run() {
104         try {
105             if (!multiProcessChecks()) {
106                 return;
107             }
108             while (true) {
109 
110                 try {
111                     // Migrate the database to the no-backup directory if necessary.
112                     // Migrations are not retry-able. So if something unexpected were to happen
113                     // here, the best we can do is to hand things off to the
114                     // InitializationExceptionHandler.
115                     WorkDatabasePathHelper.migrateDatabase(mContext);
116                 } catch (SQLiteException sqLiteException) {
117                     // This should typically never happen.
118                     String message = "Unexpected SQLite exception during migrations";
119                     Logger.get().error(TAG, message);
120                     IllegalStateException exception =
121                             new IllegalStateException(message, sqLiteException);
122                     Consumer<Throwable> exceptionHandler =
123                             mWorkManager.getConfiguration().getInitializationExceptionHandler();
124                     if (exceptionHandler != null) {
125                         exceptionHandler.accept(exception);
126                         break;
127                     } else {
128                         throw exception;
129                     }
130                 }
131 
132                 // Clean invalid jobs attributed to WorkManager, and Workers that might have been
133                 // interrupted because the application crashed (RUNNING state).
134                 Logger.get().debug(TAG, "Performing cleanup operations.");
135                 try {
136                     forceStopRunnable();
137                     break;
138                 } catch (SQLiteAccessPermException
139                          | SQLiteCantOpenDatabaseException
140                          | SQLiteConstraintException
141                          | SQLiteDatabaseCorruptException
142                          | SQLiteDatabaseLockedException
143                          | SQLiteDiskIOException
144                          | SQLiteFullException
145                          | SQLiteTableLockedException exception) {
146                     mRetryCount++;
147                     if (mRetryCount >= MAX_ATTEMPTS) {
148                         // ForceStopRunnable is usually the first thing that accesses a database
149                         // (or an app's internal data directory). This means that weird
150                         // PackageManager bugs are attributed to ForceStopRunnable, which is
151                         // unfortunate. This gives the developer a better error
152                         // message.
153                         String message;
154                         if (UserManagerCompat.isUserUnlocked(mContext)) {
155                             message = "The file system on the device is in a bad state. "
156                                     + "WorkManager cannot access the app's internal data store.";
157                         } else {
158                             message = "WorkManager can't be accessed from direct boot, because "
159                                     + "credential encrypted storage isn't accessible.\n"
160                                     + "Don't access or initialise WorkManager from directAware "
161                                     + "components. See "
162                                     + "https://developer.android.com/training/articles/direct-boot";
163                         }
164                         Logger.get().error(TAG, message, exception);
165                         IllegalStateException throwable = new IllegalStateException(message,
166                                 exception);
167                         Consumer<Throwable> exceptionHandler =
168                                 mWorkManager.getConfiguration().getInitializationExceptionHandler();
169                         if (exceptionHandler != null) {
170                             Logger.get().debug(TAG,
171                                     "Routing exception to the specified exception handler",
172                                     throwable);
173                             exceptionHandler.accept(throwable);
174                             break;
175                         } else {
176                             throw throwable;
177                         }
178                     } else {
179                         long duration = mRetryCount * BACKOFF_DURATION_MS;
180                         Logger.get()
181                                 .debug(TAG, "Retrying after " + duration,
182                                         exception);
183                         sleep(mRetryCount * BACKOFF_DURATION_MS);
184                     }
185                 }
186             }
187         } finally {
188             mWorkManager.onForceStopRunnableCompleted();
189         }
190     }
191 
192     /**
193      * @return {@code true} If the application was force stopped.
194      */
195     @VisibleForTesting
isForceStopped()196     public boolean isForceStopped() {
197         // Alarms get cancelled when an app is force-stopped starting at Eclair MR1.
198         // Cancelling of Jobs on force-stop was introduced in N-MR1 (SDK 25).
199         // Even though API 23, 24 are probably safe, OEMs may choose to do
200         // something different.
201         try {
202             int flags = FLAG_NO_CREATE;
203             if (Build.VERSION.SDK_INT >= 31) {
204                 flags |= FLAG_MUTABLE;
205             }
206             PendingIntent pendingIntent = getPendingIntent(mContext, flags);
207             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
208                 // We no longer need the alarm.
209                 if (pendingIntent != null) {
210                     pendingIntent.cancel();
211                 }
212                 ActivityManager activityManager =
213                         (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
214                 List<ApplicationExitInfo> exitInfoList =
215                         activityManager.getHistoricalProcessExitReasons(
216                                 null /* match caller uid */,
217                                 0, // ignore
218                                 0 // ignore
219                         );
220 
221                 if (exitInfoList != null && !exitInfoList.isEmpty()) {
222                     long timestamp = mPreferenceUtils.getLastForceStopEventMillis();
223                     for (int i = 0; i < exitInfoList.size(); i++) {
224                         ApplicationExitInfo info = exitInfoList.get(i);
225                         if (info.getReason() == REASON_USER_REQUESTED
226                                 && info.getTimestamp() >= timestamp) {
227                             return true;
228                         }
229                     }
230                 }
231             } else if (pendingIntent == null) {
232                 setAlarm(mContext);
233                 return true;
234             }
235             return false;
236         } catch (SecurityException | IllegalArgumentException exception) {
237             // b/189975360 Some Samsung Devices seem to throw an IllegalArgumentException :( on
238             // API 30.
239 
240             // Setting Alarms on some devices fails due to OEM introduced bugs in AlarmManager.
241             // When this happens, there is not much WorkManager can do, other can reschedule
242             // everything.
243             Logger.get().warning(TAG, "Ignoring exception", exception);
244             return true;
245         }
246     }
247 
248     /**
249      * Performs all the necessary steps to initialize {@link androidx.work.WorkManager}/
250      */
251     @VisibleForTesting
forceStopRunnable()252     public void forceStopRunnable() {
253         boolean needsScheduling = cleanUp();
254         if (shouldRescheduleWorkers()) {
255             Logger.get().debug(TAG, "Rescheduling Workers.");
256             mWorkManager.rescheduleEligibleWork();
257             // Mark the jobs as migrated.
258             mWorkManager.getPreferenceUtils().setNeedsReschedule(false);
259         } else if (isForceStopped()) {
260             Logger.get().debug(TAG, "Application was force-stopped, rescheduling.");
261             mWorkManager.rescheduleEligibleWork();
262             // Update the last known force-stop event timestamp.
263             mPreferenceUtils.setLastForceStopEventMillis(
264                     mWorkManager.getConfiguration().getClock().currentTimeMillis());
265         } else if (needsScheduling) {
266             Logger.get().debug(TAG, "Found unfinished work, scheduling it.");
267             Schedulers.schedule(
268                     mWorkManager.getConfiguration(),
269                     mWorkManager.getWorkDatabase(),
270                     mWorkManager.getSchedulers());
271         }
272     }
273 
274     /**
275      * Performs cleanup operations like
276      *
277      * * Cancel invalid JobScheduler jobs.
278      * * Reschedule previously RUNNING jobs.
279      *
280      * @return {@code true} if there are WorkSpecs that need rescheduling.
281      */
282     @SuppressWarnings("deprecation")
283     @VisibleForTesting
cleanUp()284     public boolean cleanUp() {
285         boolean needsReconciling = false;
286         if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
287             // Mitigation for faulty implementations of JobScheduler (b/134058261) and
288             // Mitigation for a platform bug, which causes jobs to get dropped when binding to
289             // SystemJobService fails.
290             needsReconciling = SystemJobScheduler.reconcileJobs(mContext,
291                     mWorkManager.getWorkDatabase());
292         }
293         // Reset previously unfinished work.
294         WorkDatabase workDatabase = mWorkManager.getWorkDatabase();
295         WorkSpecDao workSpecDao = workDatabase.workSpecDao();
296         WorkProgressDao workProgressDao = workDatabase.workProgressDao();
297         workDatabase.beginTransaction();
298         boolean needsScheduling;
299         try {
300             List<WorkSpec> workSpecs = workSpecDao.getRunningWork();
301             needsScheduling = workSpecs != null && !workSpecs.isEmpty();
302             if (needsScheduling) {
303                 // Mark every instance of unfinished work with state = ENQUEUED and
304                 // SCHEDULE_NOT_REQUESTED_AT = -1 irrespective of its current state.
305                 // This is because the application might have crashed previously and we should
306                 // reschedule jobs that may have been running previously.
307                 // Also there is a chance that an application crash, happened during
308                 // onStartJob() and now no corresponding job now exists in JobScheduler.
309                 // To solve this, we simply force-reschedule all unfinished work.
310                 for (WorkSpec workSpec : workSpecs) {
311                     workSpecDao.setState(ENQUEUED, workSpec.id);
312                     workSpecDao.setStopReason(workSpec.id, WorkInfo.STOP_REASON_UNKNOWN);
313                     workSpecDao.markWorkSpecScheduled(workSpec.id, SCHEDULE_NOT_REQUESTED_YET);
314                 }
315             }
316             workProgressDao.deleteAll();
317             workDatabase.setTransactionSuccessful();
318         } finally {
319             workDatabase.endTransaction();
320         }
321         return needsScheduling || needsReconciling;
322     }
323 
324     /**
325      * @return {@code true} If we need to reschedule Workers.
326      */
327     @VisibleForTesting
shouldRescheduleWorkers()328     public boolean shouldRescheduleWorkers() {
329         return mWorkManager.getPreferenceUtils().getNeedsReschedule();
330     }
331 
332     /**
333      * @return {@code true} if we are allowed to run in the current app process.
334      */
335     @VisibleForTesting
multiProcessChecks()336     public boolean multiProcessChecks() {
337         // Ideally we should really check if RemoteWorkManager.getInstance() is non-null.
338         // But ForceStopRunnable causes a lot of multi-process contention on the underlying
339         // SQLite datastore. Therefore we only initialize WorkManager in the default app-process.
340         Configuration configuration = mWorkManager.getConfiguration();
341         // Check if the application specified a default process name. If they did not, we want to
342         // run ForceStopRunnable in every app process. This is safer for apps with multiple
343         // processes. There is risk of SQLite contention and that might result in a crash, but an
344         // actual crash is better than decreased throughput for WorkRequests.
345         if (TextUtils.isEmpty(configuration.getDefaultProcessName())) {
346             Logger.get().debug(TAG, "The default process name was not specified.");
347             return true;
348         }
349         boolean isDefaultProcess = ProcessUtils.isDefaultProcess(mContext, configuration);
350         Logger.get().debug(TAG, "Is default app process = " + isDefaultProcess);
351         return isDefaultProcess;
352     }
353 
354     /**
355      * Helps with backoff when exceptions occur during {@link androidx.work.WorkManager}
356      * initialization.
357      */
358     @VisibleForTesting
sleep(long duration)359     public void sleep(long duration) {
360         try {
361             Thread.sleep(duration);
362         } catch (InterruptedException ignore) {
363             // Nothing to do really.
364         }
365     }
366 
367     /**
368      * @param flags The {@link PendingIntent} flags.
369      * @return an instance of the {@link PendingIntent}.
370      */
getPendingIntent(Context context, int flags)371     private static PendingIntent getPendingIntent(Context context, int flags) {
372         Intent intent = getIntent(context);
373         return PendingIntent.getBroadcast(context, ALARM_ID, intent, flags);
374     }
375 
376     /**
377      * @return The instance of {@link Intent} used to keep track of force stops.
378      */
379     @VisibleForTesting
getIntent(Context context)380     static Intent getIntent(Context context) {
381         Intent intent = new Intent();
382         intent.setComponent(new ComponentName(context, ForceStopRunnable.BroadcastReceiver.class));
383         intent.setAction(ACTION_FORCE_STOP_RESCHEDULE);
384         return intent;
385     }
386 
setAlarm(Context context)387     static void setAlarm(Context context) {
388         AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
389         // Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm.
390         int flags = FLAG_UPDATE_CURRENT;
391         if (Build.VERSION.SDK_INT >= 31) {
392             flags |= FLAG_MUTABLE;
393         }
394         PendingIntent pendingIntent = getPendingIntent(context, flags);
395         // OK to use System.currentTimeMillis() since this is intended only to keep the alarm
396         // scheduled ~forever and shouldn't need WorkManager to be initialized to reschedule.
397         long triggerAt = System.currentTimeMillis() + TEN_YEARS;
398         if (alarmManager != null) {
399             alarmManager.setExact(RTC_WAKEUP, triggerAt, pendingIntent);
400         }
401     }
402 
403     /**
404      * A {@link android.content.BroadcastReceiver} which takes care of recreating the
405      * long lived alarm which helps track force stops for an application.  This is the target of the
406      * alarm set by ForceStopRunnable in {@link #setAlarm(Context)}.
407      *
408      */
409     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
410     public static class BroadcastReceiver extends android.content.BroadcastReceiver {
411         private static final String TAG = Logger.tagWithPrefix("ForceStopRunnable$Rcvr");
412 
413         @Override
onReceive(@onNull Context context, @Nullable Intent intent)414         public void onReceive(@NonNull Context context, @Nullable Intent intent) {
415             // Our alarm somehow got triggered, so make sure we reschedule it.  This should really
416             // never happen because we set it so far in the future.
417             if (intent != null) {
418                 String action = intent.getAction();
419                 if (ACTION_FORCE_STOP_RESCHEDULE.equals(action)) {
420                     Logger.get().verbose(
421                             TAG,
422                             "Rescheduling alarm that keeps track of force-stops.");
423                     ForceStopRunnable.setAlarm(context);
424                 }
425             }
426         }
427     }
428 }
429