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