1 /* 2 * Copyright (C) 2019 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.cts.install.lib; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assert.fail; 23 24 import android.app.UiAutomation; 25 import android.content.BroadcastReceiver; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageInfo; 32 import android.content.pm.PackageInstaller; 33 import android.content.pm.PackageManager; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.SystemClock; 37 38 import androidx.test.InstrumentationRegistry; 39 40 import com.google.common.annotations.VisibleForTesting; 41 42 import java.io.IOException; 43 import java.lang.reflect.Field; 44 import java.util.List; 45 import java.util.concurrent.BlockingQueue; 46 import java.util.concurrent.LinkedBlockingQueue; 47 import java.util.concurrent.TimeUnit; 48 49 /** 50 * Utilities to facilitate installation in tests. 51 */ 52 public class InstallUtils { 53 private static final int NUM_MAX_POLLS = 5; 54 private static final int POLL_WAIT_TIME_MILLIS = 200; 55 private static final long GET_UIAUTOMATION_TIMEOUT_MS = 60000; 56 getUiAutomation()57 private static UiAutomation getUiAutomation() { 58 final long start = SystemClock.uptimeMillis(); 59 while (SystemClock.uptimeMillis() - start < GET_UIAUTOMATION_TIMEOUT_MS) { 60 UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 61 if (ui != null) { 62 return ui; 63 } 64 } 65 throw new AssertionError("Failed to get UiAutomation"); 66 } 67 68 /** 69 * Adopts the given shell permissions. 70 */ adoptShellPermissionIdentity(String... permissions)71 public static void adoptShellPermissionIdentity(String... permissions) { 72 getUiAutomation().adoptShellPermissionIdentity(permissions); 73 } 74 75 /** 76 * Drops all shell permissions. 77 */ dropShellPermissionIdentity()78 public static void dropShellPermissionIdentity() { 79 getUiAutomation().dropShellPermissionIdentity(); 80 } 81 /** 82 * Returns the version of the given package installed on device. 83 * Returns -1 if the package is not currently installed. 84 */ getInstalledVersion(String packageName)85 public static long getInstalledVersion(String packageName) { 86 Context context = InstrumentationRegistry.getTargetContext(); 87 PackageManager pm = context.getPackageManager(); 88 try { 89 PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); 90 return info.getLongVersionCode(); 91 } catch (PackageManager.NameNotFoundException e) { 92 return -1; 93 } 94 } 95 96 /** 97 * Waits for the given session to be marked as ready or failed and returns it. 98 */ waitForSession(int sessionId)99 public static PackageInstaller.SessionInfo waitForSession(int sessionId) { 100 BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>(); 101 BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() { 102 @Override 103 public void onReceive(Context context, Intent intent) { 104 PackageInstaller.SessionInfo info = 105 intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION); 106 if (info != null && info.getSessionId() == sessionId) { 107 if (info.isStagedSessionReady() || info.isStagedSessionFailed()) { 108 try { 109 sessionStatus.put(info); 110 } catch (InterruptedException e) { 111 throw new AssertionError(e); 112 } 113 } 114 } 115 } 116 }; 117 IntentFilter sessionUpdatedFilter = 118 new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED); 119 120 Context context = InstrumentationRegistry.getTargetContext(); 121 context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter); 122 123 PackageInstaller installer = getPackageInstaller(); 124 PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId); 125 126 try { 127 if (info.isStagedSessionReady() || info.isStagedSessionFailed()) { 128 sessionStatus.put(info); 129 } 130 info = sessionStatus.poll(60, TimeUnit.SECONDS); 131 context.unregisterReceiver(sessionUpdatedReceiver); 132 assertWithMessage("Timed out while waiting for session to get ready/failed") 133 .that(info).isNotNull(); 134 assertThat(info.getSessionId()).isEqualTo(sessionId); 135 return info; 136 } catch (InterruptedException e) { 137 throw new AssertionError(e); 138 } 139 } 140 141 /** 142 * Waits for the given session to be marked as ready. 143 * Throws an assertion if the session fails. 144 */ waitForSessionReady(int sessionId)145 public static void waitForSessionReady(int sessionId) { 146 PackageInstaller.SessionInfo info = waitForSession(sessionId); 147 // TODO: migrate to PackageInstallerSessionInfoSubject 148 if (info.isStagedSessionFailed()) { 149 throw new AssertionError(info.getStagedSessionErrorMessage()); 150 } 151 } 152 153 /** 154 * Returns the info for the given package name. 155 */ getPackageInfo(String packageName)156 public static PackageInfo getPackageInfo(String packageName) { 157 Context context = InstrumentationRegistry.getTargetContext(); 158 PackageManager pm = context.getPackageManager(); 159 try { 160 return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); 161 } catch (PackageManager.NameNotFoundException e) { 162 return null; 163 } 164 } 165 166 /** 167 * Returns the PackageInstaller instance of the current {@code Context} 168 */ getPackageInstaller()169 public static PackageInstaller getPackageInstaller() { 170 return InstrumentationRegistry.getTargetContext().getPackageManager().getPackageInstaller(); 171 } 172 173 /** 174 * Returns an existing session to actively perform work. 175 * {@see PackageInstaller#openSession} 176 */ openPackageInstallerSession(int sessionId)177 public static PackageInstaller.Session openPackageInstallerSession(int sessionId) 178 throws IOException { 179 return getPackageInstaller().openSession(sessionId); 180 } 181 182 /** 183 * Asserts that {@code result} intent has a success status. 184 */ assertStatusSuccess(Intent result)185 public static void assertStatusSuccess(Intent result) { 186 int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, 187 PackageInstaller.STATUS_FAILURE); 188 if (status == -1) { 189 throw new AssertionError("PENDING USER ACTION"); 190 } else if (status > 0) { 191 String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); 192 throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message); 193 } 194 } 195 196 /** 197 * Asserts that {@code result} intent has a failure status. 198 */ assertStatusFailure(Intent result)199 public static void assertStatusFailure(Intent result) { 200 // Pass SUCCESS as default to ensure that this doesn't accidentally pass 201 int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, 202 PackageInstaller.STATUS_SUCCESS); 203 switch (status) { 204 case -2: // PackageInstaller.STATUS_PENDING_STREAMING 205 case PackageInstaller.STATUS_PENDING_USER_ACTION: 206 throw new AssertionError("PENDING " + status); 207 case PackageInstaller.STATUS_SUCCESS: 208 throw new AssertionError("INCORRECT SUCCESS "); 209 case PackageInstaller.STATUS_FAILURE: 210 case PackageInstaller.STATUS_FAILURE_BLOCKED: 211 case PackageInstaller.STATUS_FAILURE_ABORTED: 212 case PackageInstaller.STATUS_FAILURE_INVALID: 213 case PackageInstaller.STATUS_FAILURE_CONFLICT: 214 case PackageInstaller.STATUS_FAILURE_STORAGE: 215 case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE: 216 break; 217 default: 218 String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); 219 throw new AssertionError(message == null ? "UNKNOWN STATUS" : message); 220 } 221 } 222 223 /** 224 * Commits {@link Install} but expects to fail. 225 * 226 * @param expectedThrowableClass class or superclass of the expected throwable. 227 * 228 */ commitExpectingFailure(Class expectedThrowableClass, String expectedFailMessage, Install install)229 public static void commitExpectingFailure(Class expectedThrowableClass, 230 String expectedFailMessage, Install install) { 231 assertThrows(expectedThrowableClass, expectedFailMessage, () -> install.commit()); 232 } 233 234 /** 235 * Mutates {@code installFlags} field of {@code params} by adding {@code 236 * additionalInstallFlags} to it. 237 */ 238 @VisibleForTesting mutateInstallFlags(PackageInstaller.SessionParams params, int additionalInstallFlags)239 public static void mutateInstallFlags(PackageInstaller.SessionParams params, 240 int additionalInstallFlags) { 241 final Class<?> clazz = params.getClass(); 242 Field installFlagsField; 243 try { 244 installFlagsField = clazz.getDeclaredField("installFlags"); 245 } catch (NoSuchFieldException e) { 246 throw new AssertionError("Unable to reflect over SessionParams.installFlags", e); 247 } 248 249 try { 250 int flags = installFlagsField.getInt(params); 251 flags |= additionalInstallFlags; 252 installFlagsField.setAccessible(true); 253 installFlagsField.setInt(params, flags); 254 } catch (IllegalAccessException e) { 255 throw new AssertionError("Unable to reflect over SessionParams.installFlags", e); 256 } 257 } 258 259 private static final String NO_RESPONSE = "NO RESPONSE"; 260 261 /** 262 * Calls into the test app to process user data. 263 * Asserts if the user data could not be processed or was version 264 * incompatible with the previously processed user data. 265 */ processUserData(String packageName)266 public static void processUserData(String packageName) { 267 Intent intent = new Intent(); 268 intent.setComponent(new ComponentName(packageName, 269 "com.android.cts.install.lib.testapp.ProcessUserData")); 270 intent.setAction("PROCESS_USER_DATA"); 271 Context context = InstrumentationRegistry.getTargetContext(); 272 273 HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread"); 274 handlerThread.start(); 275 276 // It can sometimes take a while after rollback before the app will 277 // receive this broadcast, so try a few times in a loop. 278 String result = NO_RESPONSE; 279 for (int i = 0; i < NUM_MAX_POLLS; ++i) { 280 BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>(); 281 context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { 282 @Override 283 public void onReceive(Context context, Intent intent) { 284 if (getResultCode() == 1) { 285 resultQueue.add("OK"); 286 } else { 287 // If the test app doesn't receive the broadcast or 288 // fails to set the result data, then getResultData 289 // here returns the initial NO_RESPONSE data passed to 290 // the sendOrderedBroadcast call. 291 resultQueue.add(getResultData()); 292 } 293 } 294 }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null); 295 296 try { 297 result = resultQueue.take(); 298 if (!result.equals(NO_RESPONSE)) { 299 break; 300 } 301 Thread.sleep(POLL_WAIT_TIME_MILLIS); 302 } catch (InterruptedException e) { 303 throw new AssertionError(e); 304 } 305 } 306 307 assertThat(result).isEqualTo("OK"); 308 } 309 310 /** 311 * Retrieves the app's user data version from userdata.txt. 312 * @return -1 if userdata.txt doesn't exist or -2 if the app doesn't handle the broadcast which 313 * could happen when the app crashes or doesn't start at all. 314 */ getUserDataVersion(String packageName)315 public static int getUserDataVersion(String packageName) { 316 Intent intent = new Intent(); 317 intent.setComponent(new ComponentName(packageName, 318 "com.android.cts.install.lib.testapp.ProcessUserData")); 319 intent.setAction("GET_USER_DATA_VERSION"); 320 Context context = InstrumentationRegistry.getTargetContext(); 321 322 HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread"); 323 handlerThread.start(); 324 325 // The response code returned when the broadcast is not received by the app or when the app 326 // crashes during handling the broadcast. We will retry when this code is returned. 327 final int noResponse = -2; 328 // It can sometimes take a while after rollback before the app will 329 // receive this broadcast, so try a few times in a loop. 330 BlockingQueue<Integer> resultQueue = new LinkedBlockingQueue<>(); 331 for (int i = 0; i < NUM_MAX_POLLS; ++i) { 332 context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { 333 @Override 334 public void onReceive(Context context, Intent intent) { 335 resultQueue.add(getResultCode()); 336 } 337 }, new Handler(handlerThread.getLooper()), noResponse, null, null); 338 339 try { 340 int userDataVersion = resultQueue.take(); 341 if (userDataVersion != noResponse) { 342 return userDataVersion; 343 } 344 Thread.sleep(POLL_WAIT_TIME_MILLIS); 345 } catch (InterruptedException e) { 346 throw new AssertionError(e); 347 } 348 } 349 350 return noResponse; 351 } 352 353 /** 354 * Checks whether the given package is installed on /system and was not updated. 355 */ isSystemAppWithoutUpdate(String packageName)356 static boolean isSystemAppWithoutUpdate(String packageName) { 357 PackageInfo pi = getPackageInfo(packageName); 358 if (pi == null) { 359 return false; 360 } else { 361 return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) 362 && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0); 363 } 364 } 365 366 /** 367 * Checks whether a given package is installed for only the given user, from a list of users. 368 * @param packageName the package to check 369 * @param userIdToCheck the user id of the user to check 370 * @param userIds a list of user ids to check 371 * @return {@code true} if the package is only installed for the given user, 372 * {@code false} otherwise. 373 */ isOnlyInstalledForUser(String packageName, int userIdToCheck, List<Integer> userIds)374 public static boolean isOnlyInstalledForUser(String packageName, int userIdToCheck, 375 List<Integer> userIds) { 376 Context context = InstrumentationRegistry.getTargetContext(); 377 PackageManager pm = context.getPackageManager(); 378 for (int userId: userIds) { 379 List<PackageInfo> installedPackages; 380 if (userId != userIdToCheck) { 381 installedPackages = pm.getInstalledPackagesAsUser(PackageManager.MATCH_APEX, 382 userId); 383 for (PackageInfo pi : installedPackages) { 384 if (pi.packageName.equals(packageName)) { 385 return false; 386 } 387 } 388 } 389 } 390 return true; 391 } 392 393 /** 394 * Returns the session by session Id, or null if no session is found. 395 */ getStagedSessionInfo(int sessionId)396 public static PackageInstaller.SessionInfo getStagedSessionInfo(int sessionId) { 397 PackageInstaller packageInstaller = getPackageInstaller(); 398 for (PackageInstaller.SessionInfo session : packageInstaller.getStagedSessions()) { 399 if (session.getSessionId() == sessionId) { 400 return session; 401 } 402 } 403 return null; 404 } 405 406 /** 407 * Assert that the given staged session is abandoned. The method assumes that the given session 408 * is staged. 409 * @param sessionId of the staged session 410 */ assertStagedSessionIsAbandoned(int sessionId)411 public static void assertStagedSessionIsAbandoned(int sessionId) { 412 assertThat(getStagedSessionInfo(sessionId)).isNull(); 413 } 414 415 /** 416 * A functional interface representing an operation that takes no arguments, 417 * returns no arguments and might throw a {@link Throwable} of any kind. 418 */ 419 @FunctionalInterface 420 private interface Operation { 421 /** 422 * This is the method that gets called for any object that implements this interface. 423 */ run()424 void run() throws Throwable; 425 } 426 427 /** 428 * Runs {@link Operation} and expects a {@link Throwable} of the given class to be thrown. 429 * 430 * @param expectedThrowableClass class or superclass of the expected throwable. 431 */ assertThrows(Class expectedThrowableClass, String expectedFailMessage, Operation operation)432 private static void assertThrows(Class expectedThrowableClass, String expectedFailMessage, 433 Operation operation) { 434 try { 435 operation.run(); 436 } catch (Throwable expected) { 437 assertThat(expectedThrowableClass.isAssignableFrom(expected.getClass())).isTrue(); 438 assertThat(expected.getMessage()).containsMatch(expectedFailMessage); 439 return; 440 } 441 fail("Operation was expected to fail!"); 442 } 443 } 444