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