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.policy;
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 androidx.work.WorkInfo.State.CANCELLED;
25 import static androidx.work.WorkInfo.State.FAILED;
26 import static androidx.work.WorkInfo.State.SUCCEEDED;
27 
28 import static com.android.devicelockcontroller.common.DeviceLockConstants.EXTRA_KIOSK_PACKAGE;
29 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionEvent.PROVISION_KIOSK;
30 import static com.android.devicelockcontroller.policy.ProvisionStateController.ProvisionEvent.PROVISION_PAUSE;
31 import static com.android.devicelockcontroller.provision.worker.IsDeviceInApprovedCountryWorker.KEY_IS_IN_APPROVED_COUNTRY;
32 
33 import android.app.PendingIntent;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.SharedPreferences;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageManager.NameNotFoundException;
39 import android.database.sqlite.SQLiteException;
40 import android.net.NetworkRequest;
41 import android.os.Build;
42 import android.os.Handler;
43 import android.os.Looper;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.VisibleForTesting;
47 import androidx.lifecycle.LifecycleOwner;
48 import androidx.work.BackoffPolicy;
49 import androidx.work.Constraints;
50 import androidx.work.Data;
51 import androidx.work.ExistingWorkPolicy;
52 import androidx.work.ListenableWorker;
53 import androidx.work.NetworkType;
54 import androidx.work.OneTimeWorkRequest;
55 import androidx.work.Operation;
56 import androidx.work.OutOfQuotaPolicy;
57 import androidx.work.WorkInfo;
58 import androidx.work.WorkManager;
59 import androidx.work.WorkRequest;
60 
61 import com.android.devicelockcontroller.PlayInstallPackageTaskClassProvider;
62 import com.android.devicelockcontroller.activities.DeviceLockNotificationManager;
63 import com.android.devicelockcontroller.activities.ProvisioningProgress;
64 import com.android.devicelockcontroller.activities.ProvisioningProgressController;
65 import com.android.devicelockcontroller.common.DeviceLockConstants.ProvisionFailureReason;
66 import com.android.devicelockcontroller.provision.worker.IsDeviceInApprovedCountryWorker;
67 import com.android.devicelockcontroller.provision.worker.PauseProvisioningWorker;
68 import com.android.devicelockcontroller.provision.worker.ReportDeviceProvisionStateWorker;
69 import com.android.devicelockcontroller.receivers.ResumeProvisionReceiver;
70 import com.android.devicelockcontroller.schedule.DeviceLockControllerScheduler;
71 import com.android.devicelockcontroller.schedule.DeviceLockControllerSchedulerProvider;
72 import com.android.devicelockcontroller.stats.StatsLogger;
73 import com.android.devicelockcontroller.stats.StatsLoggerProvider;
74 import com.android.devicelockcontroller.storage.GlobalParametersClient;
75 import com.android.devicelockcontroller.storage.SetupParametersClient;
76 import com.android.devicelockcontroller.util.LogUtil;
77 
78 import com.google.common.util.concurrent.FutureCallback;
79 import com.google.common.util.concurrent.Futures;
80 import com.google.common.util.concurrent.ListenableFuture;
81 
82 import java.time.Duration;
83 import java.time.LocalDateTime;
84 import java.util.UUID;
85 import java.util.concurrent.Executor;
86 import java.util.concurrent.Executors;
87 
88 /**
89  * An implementation of {@link ProvisionHelper}.
90  */
91 public final class ProvisionHelperImpl implements ProvisionHelper {
92     private static final String TAG = "ProvisionHelperImpl";
93     private static final String FILENAME = "device-lock-controller-provisioning-preferences";
94     private static final String USE_PREINSTALLED_KIOSK_PREF =
95             "debug.devicelock.usepreinstalledkiosk";
96     private static volatile SharedPreferences sSharedPreferences;
97     // For Play Install exponential backoff due to Play being updated, use a short delay of
98     // 10 seconds since the situation should resolve relatively quickly.
99     private static final Duration PLAY_INSTALL_BACKOFF_DELAY =
100             Duration.ofMillis(WorkRequest.MIN_BACKOFF_MILLIS);
101     private static final long IS_DEVICE_IN_APPROVED_COUNTRY_NETWORK_TIMEOUT_MS = 60_000;
102 
103     @VisibleForTesting
getSharedPreferences(Context context)104     static synchronized SharedPreferences getSharedPreferences(Context context) {
105         if (sSharedPreferences == null) {
106             sSharedPreferences = context.createDeviceProtectedStorageContext().getSharedPreferences(
107                     FILENAME, Context.MODE_PRIVATE);
108         }
109         return sSharedPreferences;
110     }
111 
112     private final Context mContext;
113     private final ProvisionStateController mStateController;
114     private final Executor mExecutor;
115     private final DeviceLockControllerScheduler mScheduler;
116 
ProvisionHelperImpl(Context context, ProvisionStateController stateController)117     public ProvisionHelperImpl(Context context, ProvisionStateController stateController) {
118         this(context, stateController, Executors.newCachedThreadPool());
119     }
120 
121     @VisibleForTesting
ProvisionHelperImpl(Context context, ProvisionStateController stateController, Executor executor)122     ProvisionHelperImpl(Context context, ProvisionStateController stateController,
123             Executor executor) {
124         mContext = context;
125         mStateController = stateController;
126         DeviceLockControllerSchedulerProvider schedulerProvider =
127                 (DeviceLockControllerSchedulerProvider) mContext.getApplicationContext();
128         mScheduler = schedulerProvider.getDeviceLockControllerScheduler();
129         mExecutor = executor;
130     }
131 
132     @Override
pauseProvision()133     public void pauseProvision() {
134         Futures.addCallback(Futures.transformAsync(
135                         GlobalParametersClient.getInstance().setProvisionForced(true),
136                         unused -> mStateController.setNextStateForEvent(PROVISION_PAUSE),
137                         mExecutor),
138                 new FutureCallback<>() {
139                     @Override
140                     public void onSuccess(Void unused) {
141                         createNotification();
142                         WorkManager workManager = WorkManager.getInstance(mContext);
143                         PauseProvisioningWorker.reportProvisionPausedByUser(workManager);
144                         mScheduler.scheduleResumeProvisionAlarm();
145                     }
146 
147                     @Override
148                     public void onFailure(Throwable t) {
149                         throw new RuntimeException("Failed to delay setup", t);
150                     }
151                 }, mExecutor);
152     }
153 
154     @Override
scheduleKioskAppInstallation(LifecycleOwner owner, ProvisioningProgressController progressController, boolean isMandatory)155     public void scheduleKioskAppInstallation(LifecycleOwner owner,
156             ProvisioningProgressController progressController, boolean isMandatory) {
157         LogUtil.v(TAG, "Schedule installation work");
158         progressController.setProvisioningProgress(ProvisioningProgress.GETTING_DEVICE_READY);
159         WorkManager workManager = WorkManager.getInstance(mContext);
160         OneTimeWorkRequest isDeviceInApprovedCountryWork = getIsDeviceInApprovedCountryWork();
161 
162         final ListenableFuture<Operation.State.SUCCESS> enqueueResult =
163                 workManager.enqueueUniqueWork(IsDeviceInApprovedCountryWorker.class.getSimpleName(),
164                 ExistingWorkPolicy.REPLACE, isDeviceInApprovedCountryWork).getResult();
165         Futures.addCallback(enqueueResult, new FutureCallback<Operation.State.SUCCESS>() {
166                     @Override
167                     public void onSuccess(Operation.State.SUCCESS result) {
168                         // Enqueued
169                     }
170 
171                     @Override
172                     public void onFailure(Throwable t) {
173                         LogUtil.e(TAG, "Failed to enqueue 'device in approved country' work",
174                                 t);
175                         if (t instanceof SQLiteException) {
176                             mStateController.getDevicePolicyController().wipeDevice();
177                         } else {
178                             LogUtil.e(TAG, "Not wiping device (non SQL exception)");
179                         }
180                     }
181                 }, mExecutor);
182 
183         FutureCallback<String> isInApprovedCountryCallback = new FutureCallback<>() {
184             @Override
185             public void onSuccess(String kioskPackage) {
186                 progressController.setProvisioningProgress(
187                         ProvisioningProgress.INSTALLING_KIOSK_APP);
188                 if (getPreinstalledKioskAllowed(mContext)) {
189                     try {
190                         mContext.getPackageManager().getPackageInfo(kioskPackage,
191                                 ApplicationInfo.FLAG_INSTALLED);
192                         LogUtil.i(TAG, "Kiosk app is pre-installed");
193                         progressController.setProvisioningProgress(
194                                 ProvisioningProgress.OPENING_KIOSK_APP);
195                         ReportDeviceProvisionStateWorker.reportSetupCompleted(workManager);
196                         mStateController.postSetNextStateForEventRequest(PROVISION_KIOSK);
197                     } catch (NameNotFoundException e) {
198                         LogUtil.i(TAG, "Kiosk app is not pre-installed");
199                         installFromPlay(owner, kioskPackage, isMandatory, progressController);
200                     }
201                 } else {
202                     installFromPlay(owner, kioskPackage, isMandatory, progressController);
203                 }
204             }
205 
206             @Override
207             public void onFailure(Throwable t) {
208                 LogUtil.w(TAG, "Failed to install kiosk app!", t);
209                 handleFailure(ProvisionFailureReason.PLAY_INSTALLATION_FAILED, isMandatory,
210                         progressController);
211             }
212         };
213 
214         UUID isDeviceInApprovedCountryWorkId = isDeviceInApprovedCountryWork.getId();
215         workManager.getWorkInfoByIdLiveData(isDeviceInApprovedCountryWorkId)
216                 .observe(owner, workInfo -> {
217                     if (workInfo == null) return;
218                     WorkInfo.State state = workInfo.getState();
219                     LogUtil.d(TAG, "WorkInfo changed: " + workInfo);
220                     if (state == SUCCEEDED) {
221                         if (workInfo.getOutputData().getBoolean(KEY_IS_IN_APPROVED_COUNTRY,
222                                 false)) {
223                             Futures.addCallback(
224                                     SetupParametersClient.getInstance().getKioskPackage(),
225                                     isInApprovedCountryCallback, mExecutor);
226                         } else {
227                             LogUtil.i(TAG, "Not in eligible country");
228                             handleFailure(ProvisionFailureReason.NOT_IN_ELIGIBLE_COUNTRY,
229                                     isMandatory, progressController);
230                         }
231                     } else if (state == FAILED || state == CANCELLED) {
232                         LogUtil.w(TAG, "Failed to get country eligibility!");
233                         handleFailure(ProvisionFailureReason.COUNTRY_INFO_UNAVAILABLE, isMandatory,
234                                 progressController);
235                     }
236                 });
237 
238         // If the network is not available while checking if the device is in an approved country,
239         // wait for a finite amount of time for the network to come back up, to avoid blocking
240         // indefinitely.
241         Handler handler = new Handler(Looper.getMainLooper());
242         handler.postDelayed(() -> {
243             ListenableFuture<WorkInfo> workInfoFuture =
244                     WorkManager.getInstance(mContext)
245                             .getWorkInfoById(isDeviceInApprovedCountryWorkId);
246             Futures.addCallback(workInfoFuture, new FutureCallback<>() {
247                 @Override
248                 public void onSuccess(WorkInfo workInfo) {
249                     WorkInfo.State state = workInfo.getState();
250                     if (!(state == WorkInfo.State.SUCCEEDED
251                             || state == WorkInfo.State.FAILED
252                             || state == WorkInfo.State.CANCELLED)) {
253                         LogUtil.e(TAG, "Cannot determine if device "
254                                 + "is in an approved country, cancelling job");
255                         WorkManager.getInstance(mContext)
256                                 .cancelWorkById(isDeviceInApprovedCountryWorkId);
257                     }
258                 }
259 
260                 @Override
261                 public void onFailure(Throwable t) {
262                     LogUtil.e(TAG, "Cannot determine work state for device in approved "
263                             + "country", t);
264                 }
265             }, mExecutor);
266         }, IS_DEVICE_IN_APPROVED_COUNTRY_NETWORK_TIMEOUT_MS);
267     }
268 
installFromPlay(LifecycleOwner owner, String kioskPackage, boolean isMandatory, ProvisioningProgressController progressController)269     private void installFromPlay(LifecycleOwner owner, String kioskPackage, boolean isMandatory,
270             ProvisioningProgressController progressController) {
271         Context applicationContext = mContext.getApplicationContext();
272         final Class<? extends ListenableWorker> playInstallTaskClass =
273                 ((PlayInstallPackageTaskClassProvider) applicationContext)
274                         .getPlayInstallPackageTaskClass();
275         if (playInstallTaskClass == null) {
276             LogUtil.w(TAG, "Play installation not supported!");
277             handleFailure(
278                     ProvisionFailureReason.PLAY_TASK_UNAVAILABLE, isMandatory, progressController);
279             return;
280         }
281         OneTimeWorkRequest playInstallPackageTask =
282                 getPlayInstallPackageTask(playInstallTaskClass, kioskPackage);
283         WorkManager workManager = WorkManager.getInstance(mContext);
284         final ListenableFuture<Operation.State.SUCCESS> enqueueResult =
285                 workManager.enqueueUniqueWork(playInstallTaskClass.getSimpleName(),
286                         ExistingWorkPolicy.REPLACE, playInstallPackageTask).getResult();
287         Futures.addCallback(enqueueResult, new FutureCallback<Operation.State.SUCCESS>() {
288             @Override
289             public void onSuccess(Operation.State.SUCCESS result) {
290                 // Enqueued
291             }
292 
293             @Override
294             public void onFailure(Throwable t) {
295                 LogUtil.e(TAG, "Failed to enqueue 'play install' work", t);
296                 if (t instanceof SQLiteException) {
297                     mStateController.getDevicePolicyController().wipeDevice();
298                 } else {
299                     LogUtil.e(TAG, "Not wiping device (non SQL exception)");
300                 }
301             }
302         }, mExecutor);
303 
304         mContext.getMainExecutor().execute(
305                 () -> workManager.getWorkInfoByIdLiveData(playInstallPackageTask.getId())
306                         .observe(owner, workInfo -> {
307                             if (workInfo == null) return;
308                             WorkInfo.State state = workInfo.getState();
309                             LogUtil.d(TAG, "WorkInfo changed: " + workInfo);
310                             if (state == SUCCEEDED) {
311                                 progressController.setProvisioningProgress(
312                                         ProvisioningProgress.OPENING_KIOSK_APP);
313                                 ReportDeviceProvisionStateWorker.reportSetupCompleted(workManager);
314                                 mStateController.postSetNextStateForEventRequest(PROVISION_KIOSK);
315                             } else if (state == FAILED) {
316                                 LogUtil.w(TAG, "Play installation failed!");
317                                 handleFailure(ProvisionFailureReason.PLAY_INSTALLATION_FAILED,
318                                         isMandatory, progressController);
319                             }
320                         }));
321     }
322 
handleFailure(@rovisionFailureReason int reason, boolean isMandatory, ProvisioningProgressController progressController)323     private void handleFailure(@ProvisionFailureReason int reason, boolean isMandatory,
324             ProvisioningProgressController progressController) {
325         StatsLogger logger =
326                 ((StatsLoggerProvider) mContext.getApplicationContext()).getStatsLogger();
327         switch (reason) {
328             case ProvisionFailureReason.PLAY_TASK_UNAVAILABLE -> {
329                 logger.logProvisionFailure(
330                         StatsLogger.ProvisionFailureReasonStats.PLAY_TASK_UNAVAILABLE);
331             }
332             case ProvisionFailureReason.PLAY_INSTALLATION_FAILED -> {
333                 logger.logProvisionFailure(
334                         StatsLogger.ProvisionFailureReasonStats.PLAY_INSTALLATION_FAILED);
335             }
336             case ProvisionFailureReason.COUNTRY_INFO_UNAVAILABLE -> {
337                 logger.logProvisionFailure(
338                         StatsLogger.ProvisionFailureReasonStats.COUNTRY_INFO_UNAVAILABLE);
339             }
340             case ProvisionFailureReason.NOT_IN_ELIGIBLE_COUNTRY -> {
341                 logger.logProvisionFailure(
342                         StatsLogger.ProvisionFailureReasonStats.NOT_IN_ELIGIBLE_COUNTRY);
343             }
344             case ProvisionFailureReason.POLICY_ENFORCEMENT_FAILED -> {
345                 logger.logProvisionFailure(
346                         StatsLogger.ProvisionFailureReasonStats.POLICY_ENFORCEMENT_FAILED);
347             }
348             default -> {
349                 logger.logProvisionFailure(StatsLogger.ProvisionFailureReasonStats.UNKNOWN);
350             }
351         }
352         if (isMandatory) {
353             ReportDeviceProvisionStateWorker.reportSetupFailed(
354                     WorkManager.getInstance(mContext), reason);
355             progressController.setProvisioningProgress(
356                     ProvisioningProgress.getMandatoryProvisioningFailedProgress(reason));
357             mScheduler.scheduleMandatoryResetDeviceAlarm();
358         } else {
359             // For non-mandatory provisioning, failure should only be reported after
360             // user exits the provisioning UI; otherwise, it could be reported
361             // multiple times if user choose to retry, which can break the
362             // 7-days failure flow.
363             progressController.setProvisioningProgress(
364                     ProvisioningProgress.getNonMandatoryProvisioningFailedProgress(reason));
365         }
366     }
367 
368     @NonNull
getIsDeviceInApprovedCountryWork()369     private static OneTimeWorkRequest getIsDeviceInApprovedCountryWork() {
370         NetworkRequest request = new NetworkRequest.Builder()
371                 .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
372                 .addCapability(NET_CAPABILITY_TRUSTED)
373                 .addCapability(NET_CAPABILITY_INTERNET)
374                 .addCapability(NET_CAPABILITY_NOT_VPN)
375                 .build();
376         return new OneTimeWorkRequest.Builder(IsDeviceInApprovedCountryWorker.class)
377                 .setConstraints(new Constraints.Builder().setRequiredNetworkRequest(
378                         request, NetworkType.CONNECTED).build())
379                 // Set the request as expedited and use a short retry backoff time since the
380                 // user is in the setup flow while we check if the device is in an approved country
381                 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
382                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL,
383                         Duration.ofMillis(WorkRequest.MIN_BACKOFF_MILLIS))
384                 .build();
385     }
386 
387     @NonNull
getPlayInstallPackageTask( Class<? extends ListenableWorker> playInstallTaskClass, String kioskPackageName)388     private static OneTimeWorkRequest getPlayInstallPackageTask(
389             Class<? extends ListenableWorker> playInstallTaskClass, String kioskPackageName) {
390         return new OneTimeWorkRequest.Builder(playInstallTaskClass)
391                 .setInputData(new Data.Builder().putString(
392                         EXTRA_KIOSK_PACKAGE, kioskPackageName).build())
393                 .setConstraints(new Constraints.Builder().setRequiredNetworkType(
394                         NetworkType.CONNECTED).build())
395                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, PLAY_INSTALL_BACKOFF_DELAY)
396                 .build();
397     }
398 
createNotification()399     private void createNotification() {
400         LogUtil.d(TAG, "createNotification");
401         Context context = mContext;
402 
403         PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
404                 /* requestCode= */ 0, new Intent(context, ResumeProvisionReceiver.class),
405                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
406         LocalDateTime resumeDateTime = LocalDateTime.now().plusHours(1);
407         DeviceLockNotificationManager.getInstance()
408                 .sendDeferredProvisioningNotification(context, resumeDateTime, pendingIntent);
409     }
410 
411     /**
412      * Sets whether provisioning should skip play install if there is already a preinstalled kiosk
413      * app.
414      */
setPreinstalledKioskAllowed(Context context, boolean enabled)415     public static void setPreinstalledKioskAllowed(Context context, boolean enabled) {
416         getSharedPreferences(context).edit().putBoolean(USE_PREINSTALLED_KIOSK_PREF, enabled)
417                 .apply();
418     }
419 
420     /**
421      * Returns true if provisioning should skip play install if there is already a preinstalled
422      * kiosk app. By default, this returns true for debuggable build.
423      */
getPreinstalledKioskAllowed(Context context)424     private static boolean getPreinstalledKioskAllowed(Context context) {
425         return Build.isDebuggable() && getSharedPreferences(context).getBoolean(
426                 USE_PREINSTALLED_KIOSK_PREF, Build.isDebuggable());
427     }
428 }
429