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