• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.PendingIntent.FLAG_MUTABLE;
20 import static android.app.PendingIntent.FLAG_ONE_SHOT;
21 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
22 import static android.content.pm.ApplicationInfo.FLAG_INSTALLED;
23 import static android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE;
24 import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN;
25 
26 import static com.android.devicelockcontroller.common.DeviceLockConstants.EXTRA_KIOSK_PACKAGE;
27 
28 import android.app.PendingIntent;
29 import android.content.BroadcastReceiver;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.content.IntentSender;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageInstaller;
36 import android.content.pm.PackageManager;
37 import android.content.pm.PackageManager.PackageInfoFlags;
38 import android.os.Handler;
39 import android.os.Looper;
40 import android.text.TextUtils;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.work.WorkerParameters;
45 
46 import com.android.devicelockcontroller.util.LogUtil;
47 
48 import com.google.common.annotations.VisibleForTesting;
49 import com.google.common.util.concurrent.FluentFuture;
50 import com.google.common.util.concurrent.ListenableFuture;
51 import com.google.common.util.concurrent.ListeningExecutorService;
52 import com.google.common.util.concurrent.MoreExecutors;
53 import com.google.common.util.concurrent.SettableFuture;
54 
55 import java.util.Locale;
56 
57 /**
58  * Install an existing package for a secondary user.
59  */
60 public final class InstallExistingPackageTask extends AbstractTask {
61     private static final String TAG = "InstallExistingPackageTask";
62 
63     @VisibleForTesting
64     static final String ACTION_INSTALL_EXISTING_APP_COMPLETE =
65             "com.android.devicelockcontroller.policy.ACTION_INSTALL_EXISTING_APP_COMPLETE";
66 
67     private final Context mContext;
68     private final ListeningExecutorService mExecutorService;
69     private final InstallExistingPackageCompleteBroadcastReceiver mBroadcastReceiver;
70     private final PackageInstallerWrapper mPackageInstaller;
71     private final PackageInstallPendingIntentProvider mPackageInstallPendingIntentProvider;
72     private final String mPackageName;
73 
InstallExistingPackageTask(Context context, WorkerParameters workerParameters, ListeningExecutorService executorService)74     public InstallExistingPackageTask(Context context, WorkerParameters workerParameters,
75             ListeningExecutorService executorService) {
76         this(context, workerParameters, executorService,
77                 new InstallExistingPackageCompleteBroadcastReceiver(context),
78                 new PackageInstallerWrapper(context.getPackageManager().getPackageInstaller()),
79                 new PackageInstallPendingIntentProviderImpl(context));
80     }
81 
82     @VisibleForTesting
InstallExistingPackageTask(Context context, WorkerParameters workerParameters, ListeningExecutorService executorService, InstallExistingPackageCompleteBroadcastReceiver broadcastReceiver, PackageInstallerWrapper packageInstaller, PackageInstallPendingIntentProvider packageInstallPendingIntentProvider)83     InstallExistingPackageTask(Context context, WorkerParameters workerParameters,
84             ListeningExecutorService executorService,
85             InstallExistingPackageCompleteBroadcastReceiver broadcastReceiver,
86             PackageInstallerWrapper packageInstaller,
87             PackageInstallPendingIntentProvider packageInstallPendingIntentProvider) {
88         super(context, workerParameters);
89 
90         mContext = context;
91         mExecutorService = executorService;
92         mBroadcastReceiver = broadcastReceiver;
93         mPackageInstaller = packageInstaller;
94         mPackageInstallPendingIntentProvider = packageInstallPendingIntentProvider;
95         mPackageName = workerParameters.getInputData().getString(EXTRA_KIOSK_PACKAGE);
96     }
97 
isPackageInstalled(Context context, String packageName)98     private static boolean isPackageInstalled(Context context, String packageName) {
99         final PackageManager pm = context.getPackageManager();
100         try {
101             // Requires permission QUERY_ALL_PACKAGES
102             final PackageInfo packageInfo =
103                     pm.getPackageInfo(packageName, PackageInfoFlags.of(0));
104             return (packageInfo != null)
105                     && ((packageInfo.applicationInfo.flags & FLAG_INSTALLED) != 0);
106         } catch (PackageManager.NameNotFoundException e) {
107             return false;
108         }
109     }
110 
111     @NonNull
112     @Override
startWork()113     public ListenableFuture<Result> startWork() {
114         return mExecutorService.submit(
115                 () -> {
116                     LogUtil.i(TAG, "Starts to run");
117 
118                     if (TextUtils.isEmpty(mPackageName)) {
119                         LogUtil.e(TAG, "The package name is null or empty");
120                         return failure(ERROR_CODE_NO_PACKAGE_NAME);
121                     }
122 
123                     if (isPackageInstalled(mContext, mPackageName)) {
124                         LogUtil.i(TAG, "Package already installed");
125 
126                         return Result.success();
127                     }
128 
129                     mContext.registerReceiver(mBroadcastReceiver,
130                             new IntentFilter(ACTION_INSTALL_EXISTING_APP_COMPLETE),
131                             Context.RECEIVER_NOT_EXPORTED);
132 
133                     final PendingIntent pendingIntent =
134                             mPackageInstallPendingIntentProvider.get();
135                     if (pendingIntent != null) {
136                         mBroadcastReceiver.startPeriodicTask(mPackageName);
137                         mPackageInstaller.installExistingPackage(mPackageName,
138                                 INSTALL_REASON_UNKNOWN, pendingIntent.getIntentSender());
139                     } else {
140                         LogUtil.e(TAG, "Unable to get pending intent");
141 
142                         return failure(ERROR_CODE_GET_PENDING_INTENT_FAILED);
143                     }
144                     return FluentFuture
145                             .from(mBroadcastReceiver.getFuture())
146                             .transform(success -> {
147                                 if (success == null || !success) {
148                                     LogUtil.e(TAG, String.format(Locale.US,
149                                             "InstallExistingPackageCompleteBroadcastReceiver "
150                                                     + "returned result: %b", success));
151                                     return failure(ERROR_CODE_INSTALLATION_FAILED);
152                                 } else {
153                                     LogUtil.i(TAG,
154                                             "InstallExistingPackageCompleteBroadcastReceiver "
155                                                     + "returned result: true");
156                                     return Result.success();
157                                 }
158                             }, MoreExecutors.directExecutor()).get();
159                 });
160     }
161 
162     /** Provides a pending intent for a given sessionId from PackageInstaller. */
163     interface PackageInstallPendingIntentProvider {
164         /**
165          * Returns a pending intent for a given sessionId from PackageInstaller.
166          */
167         @Nullable
168         PendingIntent get();
169     }
170 
171     /** Default implementation which returns pending intent for package install. */
172     static final class PackageInstallPendingIntentProviderImpl
173             implements PackageInstallPendingIntentProvider {
174         private final Context mContext;
175 
176         PackageInstallPendingIntentProviderImpl(Context context) {
177             mContext = context;
178         }
179 
180         @Nullable
181         @Override
182         public PendingIntent get() {
183             return PendingIntent.getBroadcast(mContext, /* requestCode */ 0,
184                     new Intent(ACTION_INSTALL_EXISTING_APP_COMPLETE)
185                             .setPackage(mContext.getPackageName()),
186                     FLAG_MUTABLE | FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT);
187         }
188     }
189 
190     /**
191      * A broadcast receiver which handles the broadcast intent when package installation is
192      * complete.
193      * The broadcast receiver will use the {@link PackageInstaller#EXTRA_STATUS} field to determine
194      * if the installation is successful.
195      * Note that installExistingPackage only sends the broadcast on success, and therefore we have
196      * an alternative way of detecting failures using polling and a timeout.
197      */
198     static final class InstallExistingPackageCompleteBroadcastReceiver extends BroadcastReceiver {
199         @VisibleForTesting
200         final SettableFuture<Boolean> mFuture = SettableFuture.create();
201 
202         private Context mContext;
203         private int mCounter = 0;
204         private final Handler mHandler;
205         private boolean mIsTaskRunning = false;
206         private String mPackageName;
207         private static final int ITERATIONS = 30;
208         private static final int INTERVAL_MS = 1000;
209 
210         InstallExistingPackageCompleteBroadcastReceiver(Context context) {
211             super();
212 
213             mContext = context;
214             mHandler = new Handler(Looper.getMainLooper());
215         }
216 
217         private final Runnable mPeriodicTask = new Runnable() {
218             @Override
219             public void run() {
220                 // Already set in onReceive.
221                 if (mFuture.isDone()) {
222                     return;
223                 }
224 
225                 executeTask();
226                 mCounter++;
227 
228                 if (mCounter <= ITERATIONS && mIsTaskRunning) {
229                     mHandler.postDelayed(mPeriodicTask, INTERVAL_MS);
230                 } else {
231                     LogUtil.e(TAG, "Timed out waiting for package installation");
232                     stopPeriodicTask();
233                     mFuture.set(false);
234                     mContext.unregisterReceiver(
235                             InstallExistingPackageCompleteBroadcastReceiver.this);
236                 }
237             }
238         };
239 
240         private void executeTask() {
241             if (isPackageInstalled(mContext, mPackageName)) {
242                 mFuture.set(true);
243                 mContext.unregisterReceiver(this);
244             }
245         }
246 
247         private void startPeriodicTask() {
248             if (!mIsTaskRunning) {
249                 mIsTaskRunning = true;
250                 mCounter = 0;
251                 mHandler.postDelayed(mPeriodicTask, INTERVAL_MS);
252             }
253         }
254 
255         public void startPeriodicTask(String packageName) {
256             mHandler.post(() -> {
257                 mPackageName = packageName;
258                 startPeriodicTask();
259             });
260         }
261 
262         private void stopPeriodicTask() {
263             if (mIsTaskRunning) {
264                 mIsTaskRunning = false;
265                 mHandler.removeCallbacks(mPeriodicTask);
266             }
267         }
268 
269         @Override
270         public void onReceive(Context context, Intent intent) {
271             stopPeriodicTask();
272 
273             // Already set by the periodic task.
274             if (mFuture.isDone()) {
275                 return;
276             }
277 
278             final int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS,
279                     PackageInstaller.STATUS_FAILURE);
280 
281             context.unregisterReceiver(this);
282             if (status == PackageInstaller.STATUS_SUCCESS) {
283                 LogUtil.i(TAG, "Package installation succeed");
284                 mFuture.set(true);
285             } else {
286                 LogUtil.e(TAG, String.format(Locale.US,
287                         "Package installation failed: status= %d, status message= %s",
288                         status, intent.getStringExtra(EXTRA_STATUS_MESSAGE)));
289                 mFuture.set(false);
290             }
291         }
292 
293         ListenableFuture<Boolean> getFuture() {
294             return mFuture;
295         }
296     }
297 
298     /**
299      * Wrapper for {@link PackageInstaller}, used for testing purpose, especially for failure
300      * testing.
301      */
302     static class PackageInstallerWrapper {
303         private final PackageInstaller mPackageInstaller;
304 
305         PackageInstallerWrapper(PackageInstaller packageInstaller) {
306             mPackageInstaller = packageInstaller;
307         }
308 
309         void installExistingPackage(String packageName, int installReason,
310                 IntentSender statusReceiver) {
311             mPackageInstaller.installExistingPackage(packageName, installReason, statusReceiver);
312         }
313     }
314 }
315