1 /* 2 * Copyright (C) 2023 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 com.android.devicelockcontroller; 18 19 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState.UNPROVISIONED; 20 21 import android.annotation.IntDef; 22 import android.app.AlarmManager; 23 import android.app.PendingIntent; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.sqlite.SQLiteException; 28 import android.os.Bundle; 29 import android.os.SystemClock; 30 31 import androidx.annotation.VisibleForTesting; 32 33 import com.android.devicelockcontroller.policy.DevicePolicyController; 34 import com.android.devicelockcontroller.policy.DeviceStateController; 35 import com.android.devicelockcontroller.policy.PolicyObjectsProvider; 36 import com.android.devicelockcontroller.policy.ProvisionStateController; 37 import com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionState; 38 import com.android.devicelockcontroller.schedule.DeviceLockControllerScheduler; 39 import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerProvider; 40 import com.android.devicelockcontroller.storage.GlobalParametersClient; 41 import com.android.devicelockcontroller.util.LogUtil; 42 43 import com.google.common.util.concurrent.FutureCallback; 44 import com.google.common.util.concurrent.Futures; 45 import com.google.common.util.concurrent.ListenableFuture; 46 47 import java.lang.annotation.ElementType; 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.lang.annotation.Target; 51 import java.time.Duration; 52 import java.util.concurrent.Executor; 53 import java.util.concurrent.Executors; 54 import java.util.concurrent.ThreadFactory; 55 import java.util.concurrent.atomic.AtomicInteger; 56 57 /** 58 * Attempt to recover from failed check ins due to disk full. 59 */ 60 public final class WorkManagerExceptionHandler implements Thread.UncaughtExceptionHandler { 61 private static final String TAG = "WorkManagerExceptionHandler"; 62 63 private static final long RETRY_ALARM_MILLISECONDS = Duration.ofHours(1).toMillis(); 64 65 @VisibleForTesting 66 public static final String ALARM_REASON = "ALARM_REASON"; 67 68 private final Executor mWorkManagerTaskExecutor; 69 private final Runnable mTerminateRunnable; 70 private static volatile WorkManagerExceptionHandler sWorkManagerExceptionHandler; 71 72 /** Alarm reason definitions. */ 73 @Target(ElementType.TYPE_USE) 74 @Retention(RetentionPolicy.SOURCE) 75 @IntDef({ 76 AlarmReason.INITIAL_CHECK_IN, 77 AlarmReason.RETRY_CHECK_IN, 78 AlarmReason.RESCHEDULE_CHECK_IN, 79 AlarmReason.INITIALIZATION, 80 }) 81 public @interface AlarmReason { 82 int INITIAL_CHECK_IN = 0; 83 int RETRY_CHECK_IN = 1; 84 int RESCHEDULE_CHECK_IN = 2; 85 int INITIALIZATION = 3; 86 } 87 88 /** 89 * Receiver to handle alarms scheduled upon failure to enqueue check-in work due to 90 * SQLite exceptions, or WorkManager initialization failures. 91 * This receiver tries to recover the check-in process, if still needed. 92 */ 93 public static final class WorkFailureAlarmReceiver extends BroadcastReceiver { 94 private final Executor mExecutor = Executors.newSingleThreadExecutor(); 95 96 @Override onReceive(Context context, Intent intent)97 public void onReceive(Context context, Intent intent) { 98 if (!WorkFailureAlarmReceiver.class.getName().equals(intent.getComponent() 99 .getClassName())) { 100 throw new IllegalArgumentException("Can not handle implicit intent!"); 101 } 102 103 final Bundle bundle = intent.getExtras(); 104 if (bundle == null) { 105 throw new IllegalArgumentException("Intent has no bundle"); 106 } 107 108 final @AlarmReason int alarmReason = bundle.getInt(ALARM_REASON, -1 /* undefined */); 109 if (alarmReason < 0) { 110 throw new IllegalArgumentException("Missing alarm reason"); 111 } 112 113 LogUtil.i(TAG, "Received alarm to recover from WorkManager exception with reason: " 114 + alarmReason); 115 116 final PendingResult pendingResult = goAsync(); 117 118 final ListenableFuture<Void> checkInIfNotYetProvisioned = 119 Futures.transformAsync(GlobalParametersClient.getInstance().isProvisionReady(), 120 isProvisionReady -> { 121 if (isProvisionReady) { 122 // Already provisioned, no need to check in 123 return Futures.immediateVoidFuture(); 124 } else { 125 return getCheckInFuture(context, alarmReason); 126 } 127 }, mExecutor); 128 129 Futures.addCallback(checkInIfNotYetProvisioned, new FutureCallback<>() { 130 @Override 131 public void onSuccess(Void result) { 132 LogUtil.i(TAG, "Successfully scheduled check in after WorkManager exception"); 133 pendingResult.finish(); 134 } 135 136 @Override 137 public void onFailure(Throwable t) { 138 LogUtil.e(TAG, "Failed to schedule check in after WorkManager exception", t); 139 pendingResult.finish(); 140 } 141 }, mExecutor); 142 } 143 getCheckInFuture(Context context, @AlarmReason int alarmReason)144 private ListenableFuture<Void> getCheckInFuture(Context context, 145 @AlarmReason int alarmReason) { 146 final DeviceLockControllerSchedulerProvider schedulerProvider = 147 (DeviceLockControllerSchedulerProvider) context.getApplicationContext(); 148 final DeviceLockControllerScheduler scheduler = 149 schedulerProvider.getDeviceLockControllerScheduler(); 150 151 ListenableFuture<Void> checkInOperation; 152 153 switch (alarmReason) { 154 case AlarmReason.INITIAL_CHECK_IN: 155 case AlarmReason.INITIALIZATION: 156 checkInOperation = scheduler.maybeScheduleInitialCheckIn(); 157 break; 158 case AlarmReason.RETRY_CHECK_IN: 159 // Use zero as delay since this is a corner case. We will eventually get the 160 // proper value from the server. 161 checkInOperation = scheduler.scheduleRetryCheckInWork(Duration.ZERO); 162 break; 163 case AlarmReason.RESCHEDULE_CHECK_IN: 164 checkInOperation = scheduler.notifyNeedRescheduleCheckIn(); 165 break; 166 default: 167 throw new IllegalArgumentException("Invalid alarm reason"); 168 } 169 170 return checkInOperation; 171 } 172 } 173 createWorkManagerTaskExecutor(Context context)174 private Executor createWorkManagerTaskExecutor(Context context) { 175 final ThreadFactory threadFactory = new ThreadFactory() { 176 private final AtomicInteger mThreadCount = new AtomicInteger(0); 177 @Override 178 public Thread newThread(Runnable r) { 179 Thread thread = new DlcWmThread(context, r, 180 "DLC.WorkManager.task-" + mThreadCount.incrementAndGet()); 181 thread.setUncaughtExceptionHandler(WorkManagerExceptionHandler.this); 182 return thread; 183 } 184 }; 185 // Same as the one used by WorkManager internally. 186 return Executors.newFixedThreadPool(Math.max(2, 187 Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)), threadFactory); 188 } 189 190 private static final class DlcWmThread extends Thread { 191 private final Thread.UncaughtExceptionHandler mOriginalUncaughtExceptionHandler; 192 private final Context mContext; 193 DlcWmThread(Context context, Runnable target, String name)194 DlcWmThread(Context context, Runnable target, String name) { 195 super(target, name); 196 mContext = context; 197 mOriginalUncaughtExceptionHandler = getUncaughtExceptionHandler(); 198 } 199 getOriginalUncaughtExceptionHandler()200 UncaughtExceptionHandler getOriginalUncaughtExceptionHandler() { 201 return mOriginalUncaughtExceptionHandler; 202 } 203 getContext()204 Context getContext() { 205 return mContext; 206 } 207 } 208 209 /** 210 * Schedule an alarm to restart the check in process in case of critical failures. 211 * This is called if we failed to enqueue the check in work. 212 */ scheduleAlarm(Context context, @AlarmReason int alarmReason)213 public static void scheduleAlarm(Context context, @AlarmReason int alarmReason) { 214 final AlarmManager alarmManager = context.getSystemService(AlarmManager.class); 215 final Intent intent = new Intent(context, WorkFailureAlarmReceiver.class); 216 final Bundle bundle = new Bundle(); 217 bundle.putInt(ALARM_REASON, alarmReason); 218 intent.putExtras(bundle); 219 final PendingIntent alarmIntent = 220 PendingIntent.getBroadcast(context, /* requestCode = */ 0, intent, 221 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 222 223 alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() 224 + RETRY_ALARM_MILLISECONDS, alarmIntent); 225 LogUtil.i(TAG, "Alarm scheduled, reason: " + alarmReason); 226 } 227 228 /** 229 * Schedule an alarm to restart the app in case of critical failures. 230 * This is called if we failed to initialize WorkManager. 231 */ scheduleAlarmAndTerminate(Context context, @AlarmReason int alarmReason)232 public void scheduleAlarmAndTerminate(Context context, @AlarmReason int alarmReason) { 233 scheduleAlarm(context, alarmReason); 234 // Terminate the process without calling the original uncaught exception handler, 235 // otherwise the alarm may be canceled if there are several crashes in a short period 236 // of time (similar to what happens in the force stopped case). 237 LogUtil.i(TAG, "Terminating Device Lock Controller because of a critical failure."); 238 mTerminateRunnable.run(); 239 } 240 241 @VisibleForTesting WorkManagerExceptionHandler(Context context, Runnable terminateRunnable)242 WorkManagerExceptionHandler(Context context, Runnable terminateRunnable) { 243 mWorkManagerTaskExecutor = createWorkManagerTaskExecutor(context); 244 mTerminateRunnable = terminateRunnable; 245 } 246 247 /** 248 * Get the only instance of WorkManagerExceptionHandler. 249 */ getInstance(Context context)250 public static WorkManagerExceptionHandler getInstance(Context context) { 251 if (sWorkManagerExceptionHandler == null) { 252 synchronized (WorkManagerExceptionHandler.class) { 253 if (sWorkManagerExceptionHandler == null) { 254 sWorkManagerExceptionHandler = new WorkManagerExceptionHandler(context, 255 () -> System.exit(0)); 256 } 257 } 258 } 259 260 return sWorkManagerExceptionHandler; 261 } 262 getWorkManagerTaskExecutor()263 Executor getWorkManagerTaskExecutor() { 264 return mWorkManagerTaskExecutor; 265 } 266 267 // Called when one of the internal task threads of WorkManager throws an exception. 268 // We're interested in some exceptions subclass of SQLiteException (like SQLiteFullException) 269 // since it's not handled in initializationExceptionHandler. 270 @Override uncaughtException(Thread t, Throwable e)271 public void uncaughtException(Thread t, Throwable e) { 272 LogUtil.e(TAG, "Uncaught exception in WorkManager task", e); 273 if (!(t instanceof DlcWmThread)) { 274 throw new RuntimeException("Thread is not a DlcWmThread", e); 275 } 276 277 if (e instanceof SQLiteException) { 278 handleWorkManagerException(((DlcWmThread) t).getContext(), e); 279 } else { 280 final Thread.UncaughtExceptionHandler originalExceptionHandler = 281 ((DlcWmThread) t).getOriginalUncaughtExceptionHandler(); 282 283 originalExceptionHandler.uncaughtException(t, e); 284 } 285 } 286 handleWorkManagerException(Context context, Throwable t)287 private void handleWorkManagerException(Context context, Throwable t) { 288 Futures.addCallback(handleException(context, t), new FutureCallback<>() { 289 @Override 290 public void onSuccess(Void result) { 291 // No-op 292 } 293 294 @Override 295 public void onFailure(Throwable e) { 296 LogUtil.e(TAG, "Error handling WorkManager exception", e); 297 } 298 }, mWorkManagerTaskExecutor); 299 } 300 301 // This is setup in WM configuration and is called when initialization fails. It does not 302 // include the SQLiteFullException case. initializationExceptionHandler(Context context, Throwable t)303 void initializationExceptionHandler(Context context, Throwable t) { 304 LogUtil.e(TAG, "WorkManager initialization error", t); 305 306 handleWorkManagerException(context, t); 307 } 308 309 @VisibleForTesting handleException(Context context, Throwable t)310 ListenableFuture<Void> handleException(Context context, Throwable t) { 311 final Context applicationContext = context.getApplicationContext(); 312 final PolicyObjectsProvider policyObjectsProvider = 313 (PolicyObjectsProvider) applicationContext; 314 final ProvisionStateController provisionStateController = 315 policyObjectsProvider.getProvisionStateController(); 316 final DeviceStateController deviceStateController = 317 policyObjectsProvider.getDeviceStateController(); 318 final ListenableFuture<@ProvisionState Integer> provisionStateFuture = 319 provisionStateController.getState(); 320 final ListenableFuture<Boolean> isClearedFuture = deviceStateController.isCleared(); 321 322 return Futures.whenAllSucceed(provisionStateFuture, isClearedFuture).call(() -> { 323 final @ProvisionState Integer provisionState = Futures.getDone(provisionStateFuture); 324 if (provisionState == UNPROVISIONED) { 325 scheduleAlarmAndTerminate(context, AlarmReason.INITIALIZATION); 326 } else if (!Futures.getDone(isClearedFuture)) { 327 LogUtil.e(TAG, "Resetting device, current provisioning state: " 328 + provisionState, t); 329 final DevicePolicyController devicePolicyController = 330 policyObjectsProvider.getPolicyController(); 331 devicePolicyController.wipeDevice(); 332 } else { 333 LogUtil.w(TAG, "Device won't be reset (restrictions cleared)"); 334 } 335 return null; 336 }, mWorkManagerTaskExecutor); 337 } 338 } 339