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.provision.worker;
18 
19 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
20 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
21 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
22 import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
23 
24 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_DISMISSIBLE_UI;
25 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_FACTORY_RESET;
26 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_PERSISTENT_UI;
27 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_RETRY;
28 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_SUCCESS;
29 import static com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState.PROVISION_STATE_UNSPECIFIED;
30 import static com.android.devicelockcontroller.common.DeviceLockConstants.ProvisionFailureReason.DEADLINE_PASSED;
31 
32 import android.content.Context;
33 import android.net.NetworkRequest;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.VisibleForTesting;
37 import androidx.work.BackoffPolicy;
38 import androidx.work.Constraints;
39 import androidx.work.Data;
40 import androidx.work.ExistingWorkPolicy;
41 import androidx.work.NetworkType;
42 import androidx.work.OneTimeWorkRequest;
43 import androidx.work.Operation;
44 import androidx.work.WorkManager;
45 import androidx.work.WorkerParameters;
46 
47 import com.android.devicelockcontroller.activities.DeviceLockNotificationManager;
48 import com.android.devicelockcontroller.common.DeviceLockConstants.DeviceProvisionState;
49 import com.android.devicelockcontroller.common.DeviceLockConstants.ProvisionFailureReason;
50 import com.android.devicelockcontroller.provision.grpc.DeviceCheckInClient;
51 import com.android.devicelockcontroller.provision.grpc.ReportDeviceProvisionStateGrpcResponse;
52 import com.android.devicelockcontroller.schedule.DeviceLockControllerScheduler;
53 import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerProvider;
54 import com.android.devicelockcontroller.stats.StatsLogger;
55 import com.android.devicelockcontroller.stats.StatsLoggerProvider;
56 import com.android.devicelockcontroller.storage.GlobalParametersClient;
57 import com.android.devicelockcontroller.storage.SetupParametersClient;
58 import com.android.devicelockcontroller.storage.UserParameters;
59 import com.android.devicelockcontroller.util.LogUtil;
60 
61 import com.google.common.util.concurrent.FutureCallback;
62 import com.google.common.util.concurrent.Futures;
63 import com.google.common.util.concurrent.ListenableFuture;
64 import com.google.common.util.concurrent.ListeningExecutorService;
65 import com.google.common.util.concurrent.MoreExecutors;
66 
67 /**
68  * A worker class dedicated to report state of provision for the device lock program.
69  */
70 public final class ReportDeviceProvisionStateWorker extends AbstractCheckInWorker {
71     public static final String KEY_IS_PROVISION_SUCCESSFUL = "is-provision-successful";
72     public static final String KEY_PROVISION_FAILURE_REASON = "provision-failure-reason";
73     public static final String REPORT_PROVISION_STATE_WORK_NAME = "report-provision-state";
74     @VisibleForTesting
75     static final String UNEXPECTED_PROVISION_STATE_ERROR_MESSAGE = "Unexpected provision state!";
76 
77     private final StatsLogger mStatsLogger;
78 
79     /**
80      * Report provision failure and get next failed step
81      */
reportSetupFailed(WorkManager workManager, @ProvisionFailureReason int reason)82     public static void reportSetupFailed(WorkManager workManager,
83             @ProvisionFailureReason int reason) {
84         Data inputData = new Data.Builder()
85                 .putBoolean(KEY_IS_PROVISION_SUCCESSFUL, false)
86                 .putInt(KEY_PROVISION_FAILURE_REASON, reason)
87                 .build();
88         enqueueReportWork(inputData, workManager);
89     }
90 
91     /**
92      * Report provision success
93      */
reportSetupCompleted(WorkManager workManager)94     public static void reportSetupCompleted(WorkManager workManager) {
95         Data inputData = new Data.Builder()
96                 .putBoolean(KEY_IS_PROVISION_SUCCESSFUL, true)
97                 .build();
98         enqueueReportWork(inputData, workManager);
99     }
100 
101     /**
102      * Schedule a work to report the current provision failed step to server.
103      */
reportCurrentFailedStep(WorkManager workManager)104     public static void reportCurrentFailedStep(WorkManager workManager) {
105         Data inputData = new Data.Builder()
106                 .putBoolean(KEY_IS_PROVISION_SUCCESSFUL, false)
107                 .putInt(KEY_PROVISION_FAILURE_REASON, DEADLINE_PASSED)
108                 .build();
109         enqueueReportWork(inputData, workManager);
110     }
111 
enqueueReportWork(Data inputData, WorkManager workManager)112     private static void enqueueReportWork(Data inputData, WorkManager workManager) {
113         NetworkRequest request = new NetworkRequest.Builder()
114                 .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
115                 .addCapability(NET_CAPABILITY_TRUSTED)
116                 .addCapability(NET_CAPABILITY_INTERNET)
117                 .addCapability(NET_CAPABILITY_NOT_VPN)
118                 .build();
119         Constraints constraints = new Constraints.Builder()
120                 .setRequiredNetworkRequest(request, NetworkType.CONNECTED)
121                 .build();
122         OneTimeWorkRequest work =
123                 new OneTimeWorkRequest.Builder(ReportDeviceProvisionStateWorker.class)
124                         .setConstraints(constraints)
125                         .setInputData(inputData)
126                         .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY)
127                         .build();
128         ListenableFuture<Operation.State.SUCCESS> result =
129                 workManager.enqueueUniqueWork(REPORT_PROVISION_STATE_WORK_NAME,
130                 ExistingWorkPolicy.APPEND_OR_REPLACE, work).getResult();
131         Futures.addCallback(result,
132                 new FutureCallback<>() {
133                     @Override
134                     public void onSuccess(Operation.State.SUCCESS result) {
135                         // no-op
136                     }
137 
138                     @Override
139                     public void onFailure(Throwable t) {
140                         // Log an error but don't reset the device (non critical failure).
141                         LogUtil.e(TAG, "Failed to enqueue 'report provision state' work", t);
142                     }
143                 },
144                 MoreExecutors.directExecutor()
145         );
146     }
147 
ReportDeviceProvisionStateWorker(@onNull Context context, @NonNull WorkerParameters workerParams, ListeningExecutorService executorService)148     public ReportDeviceProvisionStateWorker(@NonNull Context context,
149             @NonNull WorkerParameters workerParams, ListeningExecutorService executorService) {
150         this(context, workerParams, /* client= */ null,
151                 executorService);
152     }
153 
154     @VisibleForTesting
ReportDeviceProvisionStateWorker(@onNull Context context, @NonNull WorkerParameters workerParams, DeviceCheckInClient client, ListeningExecutorService executorService)155     ReportDeviceProvisionStateWorker(@NonNull Context context,
156             @NonNull WorkerParameters workerParams, DeviceCheckInClient client,
157             ListeningExecutorService executorService) {
158         super(context, workerParams, client, executorService);
159         StatsLoggerProvider loggerProvider =
160                 (StatsLoggerProvider) context.getApplicationContext();
161         mStatsLogger = loggerProvider.getStatsLogger();
162     }
163 
164     @NonNull
165     @Override
startWork()166     public ListenableFuture<Result> startWork() {
167         GlobalParametersClient globalParametersClient = GlobalParametersClient.getInstance();
168         ListenableFuture<Integer> lastState =
169                 globalParametersClient.getLastReceivedProvisionState();
170         ListenableFuture<Boolean> isMandatory =
171                 SetupParametersClient.getInstance().isProvisionMandatory();
172         DeviceLockControllerSchedulerProvider schedulerProvider =
173                 (DeviceLockControllerSchedulerProvider) mContext;
174         DeviceLockControllerScheduler scheduler =
175                 schedulerProvider.getDeviceLockControllerScheduler();
176         return Futures.whenAllSucceed(mClient, lastState, isMandatory).call(() -> {
177             boolean isSuccessful = getInputData().getBoolean(
178                     KEY_IS_PROVISION_SUCCESSFUL, /* defaultValue= */ false);
179             int failureReason = getInputData().getInt(KEY_PROVISION_FAILURE_REASON,
180                     ProvisionFailureReason.UNKNOWN_REASON);
181             if (!isSuccessful && failureReason == ProvisionFailureReason.UNKNOWN_REASON) {
182                 LogUtil.e(TAG, "Reporting failure with an unknown reason is not allowed");
183             }
184             ReportDeviceProvisionStateGrpcResponse response =
185                     Futures.getDone(mClient).reportDeviceProvisionState(
186                             Futures.getDone(lastState),
187                             isSuccessful,
188                             failureReason);
189             if (response.hasRecoverableError()) {
190                 LogUtil.w(TAG, "Report provision state failed w/ recoverable error " + response
191                         + "\nRetrying...");
192                 return Result.retry();
193             }
194             if (response.hasFatalError()) {
195                 LogUtil.e(TAG,
196                         "Report provision state failed: " + response + "\nRetry current step");
197                 scheduler.scheduleNextProvisionFailedStepAlarm();
198                 return Result.failure();
199             }
200             int daysLeftUntilReset = response.getDaysLeftUntilReset();
201             if (daysLeftUntilReset > 0) {
202                 UserParameters.setDaysLeftUntilReset(mContext, daysLeftUntilReset);
203             }
204             int nextState = response.getNextClientProvisionState();
205             Futures.getUnchecked(globalParametersClient.setLastReceivedProvisionState(nextState));
206             mStatsLogger.logReportDeviceProvisionState();
207             if (!Futures.getDone(isMandatory)) {
208                 onNextProvisionStateReceived(nextState, daysLeftUntilReset);
209                 if (nextState == PROVISION_STATE_FACTORY_RESET) {
210                     scheduler.scheduleResetDeviceAlarm();
211                 } else if (nextState != PROVISION_STATE_SUCCESS) {
212                     scheduler.scheduleNextProvisionFailedStepAlarm();
213                 }
214             }
215             return Result.success();
216         }, mExecutorService);
217     }
218 
onNextProvisionStateReceived(@eviceProvisionState int provisionState, int daysLeftUntilReset)219     private void onNextProvisionStateReceived(@DeviceProvisionState int provisionState,
220             int daysLeftUntilReset) {
221         switch (provisionState) {
222             case PROVISION_STATE_RETRY:
223             case PROVISION_STATE_SUCCESS:
224             case PROVISION_STATE_UNSPECIFIED:
225             case PROVISION_STATE_FACTORY_RESET:
226                 // no-op
227                 break;
228             case PROVISION_STATE_DISMISSIBLE_UI:
229                 DeviceLockNotificationManager.getInstance()
230                         .sendDeviceResetNotification(mContext, daysLeftUntilReset);
231                 break;
232             case PROVISION_STATE_PERSISTENT_UI:
233                 DeviceLockNotificationManager.getInstance()
234                         .sendDeviceResetInOneDayOngoingNotification(mContext);
235                 break;
236             default:
237                 throw new IllegalStateException(UNEXPECTED_PROVISION_STATE_ERROR_MESSAGE);
238         }
239     }
240 }
241