1 /* 2 * Copyright (C) 2018 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.tests.rollback; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertNull; 22 import static org.junit.Assert.fail; 23 24 import android.app.ActivityManager; 25 import android.app.AlarmManager; 26 import android.content.BroadcastReceiver; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageInfo; 33 import android.content.pm.PackageInstaller; 34 import android.content.pm.PackageManager; 35 import android.content.pm.VersionedPackage; 36 import android.content.rollback.PackageRollbackInfo; 37 import android.content.rollback.RollbackInfo; 38 import android.content.rollback.RollbackManager; 39 import android.os.Handler; 40 import android.os.HandlerThread; 41 import android.util.Log; 42 43 import androidx.test.InstrumentationRegistry; 44 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 import java.util.Arrays; 49 import java.util.List; 50 import java.util.concurrent.BlockingQueue; 51 import java.util.concurrent.LinkedBlockingQueue; 52 import java.util.concurrent.SynchronousQueue; 53 54 /** 55 * Utilities to facilitate testing rollbacks. 56 */ 57 class RollbackTestUtils { 58 59 private static final String TAG = "RollbackTest"; 60 getRollbackManager()61 static RollbackManager getRollbackManager() { 62 Context context = InstrumentationRegistry.getContext(); 63 RollbackManager rm = (RollbackManager) context.getSystemService(Context.ROLLBACK_SERVICE); 64 if (rm == null) { 65 throw new AssertionError("Failed to get RollbackManager"); 66 } 67 return rm; 68 } 69 setTime(long millis)70 private static void setTime(long millis) { 71 Context context = InstrumentationRegistry.getContext(); 72 AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 73 am.setTime(millis); 74 } 75 forwardTimeBy(long offsetMillis)76 static void forwardTimeBy(long offsetMillis) { 77 setTime(System.currentTimeMillis() + offsetMillis); 78 Log.i(TAG, "Forwarded time on device by " + offsetMillis + " millis"); 79 } 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 static long getInstalledVersion(String packageName) { 86 PackageInfo pi = getPackageInfo(packageName); 87 if (pi == null) { 88 return -1; 89 } else { 90 return pi.getLongVersionCode(); 91 } 92 } 93 isSystemAppWithoutUpdate(String packageName)94 private static boolean isSystemAppWithoutUpdate(String packageName) { 95 PackageInfo pi = getPackageInfo(packageName); 96 if (pi == null) { 97 return false; 98 } else { 99 return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) 100 && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0); 101 } 102 } 103 getPackageInfo(String packageName)104 private static PackageInfo getPackageInfo(String packageName) { 105 Context context = InstrumentationRegistry.getContext(); 106 PackageManager pm = context.getPackageManager(); 107 try { 108 return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); 109 } catch (PackageManager.NameNotFoundException e) { 110 return null; 111 } 112 } 113 assertStatusSuccess(Intent result)114 private static void assertStatusSuccess(Intent result) { 115 int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, 116 PackageInstaller.STATUS_FAILURE); 117 if (status == -1) { 118 throw new AssertionError("PENDING USER ACTION"); 119 } else if (status > 0) { 120 String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); 121 throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message); 122 } 123 } 124 125 /** 126 * Uninstalls the given package. 127 * Does nothing if the package is not installed. 128 * @throws AssertionError if package can't be uninstalled. 129 */ uninstall(String packageName)130 static void uninstall(String packageName) throws InterruptedException, IOException { 131 // No need to uninstall if the package isn't installed or is installed on /system. 132 if (getInstalledVersion(packageName) == -1 || isSystemAppWithoutUpdate(packageName)) { 133 return; 134 } 135 136 Context context = InstrumentationRegistry.getContext(); 137 PackageManager packageManager = context.getPackageManager(); 138 PackageInstaller packageInstaller = packageManager.getPackageInstaller(); 139 packageInstaller.uninstall(packageName, LocalIntentSender.getIntentSender()); 140 assertStatusSuccess(LocalIntentSender.getIntentSenderResult()); 141 } 142 143 /** 144 * Commit the given rollback. 145 * @throws AssertionError if the rollback fails. 146 */ rollback(int rollbackId, VersionedPackage... causePackages)147 static void rollback(int rollbackId, VersionedPackage... causePackages) 148 throws InterruptedException { 149 RollbackManager rm = getRollbackManager(); 150 rm.commitRollback(rollbackId, Arrays.asList(causePackages), 151 LocalIntentSender.getIntentSender()); 152 Intent result = LocalIntentSender.getIntentSenderResult(); 153 int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, 154 RollbackManager.STATUS_FAILURE); 155 if (status != RollbackManager.STATUS_SUCCESS) { 156 String message = result.getStringExtra(RollbackManager.EXTRA_STATUS_MESSAGE); 157 throw new AssertionError(message); 158 } 159 } 160 161 /** 162 * Installs the apk with the given name. 163 * 164 * @param resourceName name of class loader resource for the apk to 165 * install. 166 * @param enableRollback if rollback should be enabled. 167 * @throws AssertionError if the installation fails. 168 */ install(String resourceName, boolean enableRollback)169 static void install(String resourceName, boolean enableRollback) 170 throws InterruptedException, IOException { 171 installSplit(enableRollback, resourceName); 172 } 173 174 /** 175 * Installs the apk with the given name and its splits. 176 * 177 * @param enableRollback if rollback should be enabled. 178 * @param resourceNames names of class loader resources for the apk and 179 * its splits to install. 180 * @throws AssertionError if the installation fails. 181 */ installSplit(boolean enableRollback, String... resourceNames)182 static void installSplit(boolean enableRollback, String... resourceNames) 183 throws InterruptedException, IOException { 184 Context context = InstrumentationRegistry.getContext(); 185 PackageInstaller.Session session = null; 186 PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); 187 PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( 188 PackageInstaller.SessionParams.MODE_FULL_INSTALL); 189 params.setEnableRollback(enableRollback); 190 int sessionId = packageInstaller.createSession(params); 191 session = packageInstaller.openSession(sessionId); 192 193 ClassLoader loader = RollbackTest.class.getClassLoader(); 194 for (String resourceName : resourceNames) { 195 try (OutputStream packageInSession = session.openWrite(resourceName, 0, -1); 196 InputStream is = loader.getResourceAsStream(resourceName);) { 197 byte[] buffer = new byte[4096]; 198 int n; 199 while ((n = is.read(buffer)) >= 0) { 200 packageInSession.write(buffer, 0, n); 201 } 202 } 203 } 204 205 // Commit the session (this will start the installation workflow). 206 session.commit(LocalIntentSender.getIntentSender()); 207 assertStatusSuccess(LocalIntentSender.getIntentSenderResult()); 208 } 209 210 /** Launches {@code packageName} with {@link Intent#ACTION_MAIN}. */ launchPackage(String packageName)211 private static void launchPackage(String packageName) 212 throws InterruptedException, IOException { 213 Context context = InstrumentationRegistry.getContext(); 214 Intent intent = new Intent(Intent.ACTION_MAIN); 215 intent.setPackage(packageName); 216 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 217 intent.addCategory(Intent.CATEGORY_LAUNCHER); 218 context.startActivity(intent); 219 } 220 221 /** 222 * Installs the APKs or APEXs with the given resource names as an atomic 223 * set. A resource is assumed to be an APEX if it has the .apex extension. 224 * <p> 225 * In case of staged installs, this function will return succesfully after 226 * the staged install has been committed and is ready for the device to 227 * reboot. 228 * 229 * @param staged if the rollback should be staged. 230 * @param enableRollback if rollback should be enabled. 231 * @param resourceNames names of the class loader resource for the apks to 232 * install. 233 * @throws AssertionError if the installation fails. 234 */ install(boolean staged, boolean enableRollback, String... resourceNames)235 private static void install(boolean staged, boolean enableRollback, 236 String... resourceNames) throws InterruptedException, IOException { 237 Context context = InstrumentationRegistry.getContext(); 238 PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); 239 240 PackageInstaller.SessionParams multiPackageParams = new PackageInstaller.SessionParams( 241 PackageInstaller.SessionParams.MODE_FULL_INSTALL); 242 multiPackageParams.setMultiPackage(); 243 if (staged) { 244 multiPackageParams.setStaged(); 245 } 246 // TODO: Do we set this on the parent params, the child params, or 247 // both? 248 multiPackageParams.setEnableRollback(enableRollback); 249 int multiPackageId = packageInstaller.createSession(multiPackageParams); 250 PackageInstaller.Session multiPackage = packageInstaller.openSession(multiPackageId); 251 252 for (String resourceName : resourceNames) { 253 PackageInstaller.Session session = null; 254 PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( 255 PackageInstaller.SessionParams.MODE_FULL_INSTALL); 256 if (staged) { 257 params.setStaged(); 258 } 259 if (resourceName.endsWith(".apex")) { 260 params.setInstallAsApex(); 261 } 262 params.setEnableRollback(enableRollback); 263 int sessionId = packageInstaller.createSession(params); 264 session = packageInstaller.openSession(sessionId); 265 266 ClassLoader loader = RollbackTest.class.getClassLoader(); 267 try (OutputStream packageInSession = session.openWrite(resourceName, 0, -1); 268 InputStream is = loader.getResourceAsStream(resourceName);) { 269 byte[] buffer = new byte[4096]; 270 int n; 271 while ((n = is.read(buffer)) >= 0) { 272 packageInSession.write(buffer, 0, n); 273 } 274 } 275 multiPackage.addChildSessionId(sessionId); 276 } 277 278 // Commit the session (this will start the installation workflow). 279 multiPackage.commit(LocalIntentSender.getIntentSender()); 280 assertStatusSuccess(LocalIntentSender.getIntentSenderResult()); 281 282 if (staged) { 283 waitForSessionReady(multiPackageId); 284 } 285 } 286 287 /** 288 * Installs the apks with the given resource names as an atomic set. 289 * 290 * @param enableRollback if rollback should be enabled. 291 * @param resourceNames names of the class loader resource for the apks to 292 * install. 293 * @throws AssertionError if the installation fails. 294 */ installMultiPackage(boolean enableRollback, String... resourceNames)295 static void installMultiPackage(boolean enableRollback, String... resourceNames) 296 throws InterruptedException, IOException { 297 install(false, enableRollback, resourceNames); 298 } 299 300 /** 301 * Installs the APKs or APEXs with the given resource names as a staged 302 * atomic set. A resource is assumed to be an APEX if it has the .apex 303 * extension. 304 * 305 * @param enableRollback if rollback should be enabled. 306 * @param resourceNames names of the class loader resource for the apks to 307 * install. 308 * @throws AssertionError if the installation fails. 309 */ installStaged(boolean enableRollback, String... resourceNames)310 static void installStaged(boolean enableRollback, String... resourceNames) 311 throws InterruptedException, IOException { 312 install(true, enableRollback, resourceNames); 313 } 314 adoptShellPermissionIdentity(String... permissions)315 static void adoptShellPermissionIdentity(String... permissions) { 316 InstrumentationRegistry 317 .getInstrumentation() 318 .getUiAutomation() 319 .adoptShellPermissionIdentity(permissions); 320 } 321 dropShellPermissionIdentity()322 static void dropShellPermissionIdentity() { 323 InstrumentationRegistry 324 .getInstrumentation() 325 .getUiAutomation() 326 .dropShellPermissionIdentity(); 327 } 328 329 /** 330 * Returns the RollbackInfo with a given package in the list of rollbacks. 331 * Throws an assertion failure if there is more than one such rollback 332 * info. Returns null if there are no such rollback infos. 333 */ getUniqueRollbackInfoForPackage(List<RollbackInfo> rollbacks, String packageName)334 static RollbackInfo getUniqueRollbackInfoForPackage(List<RollbackInfo> rollbacks, 335 String packageName) { 336 RollbackInfo found = null; 337 for (RollbackInfo rollback : rollbacks) { 338 for (PackageRollbackInfo info : rollback.getPackages()) { 339 if (packageName.equals(info.getPackageName())) { 340 assertNull(found); 341 found = rollback; 342 break; 343 } 344 } 345 } 346 return found; 347 } 348 349 /** 350 * Asserts that the given PackageRollbackInfo has the expected package 351 * name and versions. 352 */ assertPackageRollbackInfoEquals(String packageName, long versionRolledBackFrom, long versionRolledBackTo, PackageRollbackInfo info)353 static void assertPackageRollbackInfoEquals(String packageName, 354 long versionRolledBackFrom, long versionRolledBackTo, 355 PackageRollbackInfo info) { 356 assertEquals(packageName, info.getPackageName()); 357 assertEquals(packageName, info.getVersionRolledBackFrom().getPackageName()); 358 assertEquals(versionRolledBackFrom, info.getVersionRolledBackFrom().getLongVersionCode()); 359 assertEquals(packageName, info.getVersionRolledBackTo().getPackageName()); 360 assertEquals(versionRolledBackTo, info.getVersionRolledBackTo().getLongVersionCode()); 361 } 362 363 /** 364 * Asserts that the given RollbackInfo has the given packages with expected 365 * package names and all are rolled to and from the same given versions. 366 */ assertRollbackInfoEquals(String[] packageNames, long versionRolledBackFrom, long versionRolledBackTo, RollbackInfo info, VersionedPackage... causePackages)367 static void assertRollbackInfoEquals(String[] packageNames, 368 long versionRolledBackFrom, long versionRolledBackTo, 369 RollbackInfo info, VersionedPackage... causePackages) { 370 assertNotNull(info); 371 assertEquals(packageNames.length, info.getPackages().size()); 372 int foundPackages = 0; 373 for (String packageName : packageNames) { 374 for (PackageRollbackInfo pkgRollbackInfo : info.getPackages()) { 375 if (packageName.equals(pkgRollbackInfo.getPackageName())) { 376 foundPackages++; 377 assertPackageRollbackInfoEquals(packageName, versionRolledBackFrom, 378 versionRolledBackTo, pkgRollbackInfo); 379 break; 380 } 381 } 382 } 383 assertEquals(packageNames.length, foundPackages); 384 assertEquals(causePackages.length, info.getCausePackages().size()); 385 for (int i = 0; i < causePackages.length; ++i) { 386 assertEquals(causePackages[i].getPackageName(), 387 info.getCausePackages().get(i).getPackageName()); 388 assertEquals(causePackages[i].getLongVersionCode(), 389 info.getCausePackages().get(i).getLongVersionCode()); 390 } 391 } 392 393 /** 394 * Asserts that the given RollbackInfo has a single package with expected 395 * package name and versions. 396 */ assertRollbackInfoEquals(String packageName, long versionRolledBackFrom, long versionRolledBackTo, RollbackInfo info, VersionedPackage... causePackages)397 static void assertRollbackInfoEquals(String packageName, 398 long versionRolledBackFrom, long versionRolledBackTo, 399 RollbackInfo info, VersionedPackage... causePackages) { 400 String[] packageNames = {packageName}; 401 assertRollbackInfoEquals(packageNames, versionRolledBackFrom, versionRolledBackTo, info, 402 causePackages); 403 } 404 405 /** 406 * Waits for the given session to be marked as ready. 407 * Throws an assertion if the session fails. 408 */ waitForSessionReady(int sessionId)409 static void waitForSessionReady(int sessionId) { 410 BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>(); 411 BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() { 412 @Override 413 public void onReceive(Context context, Intent intent) { 414 PackageInstaller.SessionInfo info = 415 intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION); 416 if (info != null && info.getSessionId() == sessionId) { 417 if (info.isStagedSessionReady() || info.isStagedSessionFailed()) { 418 try { 419 sessionStatus.put(info); 420 } catch (InterruptedException e) { 421 Log.e(TAG, "Failed to put session info.", e); 422 } 423 } 424 } 425 } 426 }; 427 IntentFilter sessionUpdatedFilter = 428 new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED); 429 430 Context context = InstrumentationRegistry.getContext(); 431 context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter); 432 433 PackageInstaller installer = context.getPackageManager().getPackageInstaller(); 434 PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId); 435 436 try { 437 if (info.isStagedSessionReady() || info.isStagedSessionFailed()) { 438 sessionStatus.put(info); 439 } 440 441 info = sessionStatus.take(); 442 context.unregisterReceiver(sessionUpdatedReceiver); 443 if (info.isStagedSessionFailed()) { 444 throw new AssertionError(info.getStagedSessionErrorMessage()); 445 } 446 } catch (InterruptedException e) { 447 throw new AssertionError(e); 448 } 449 } 450 451 private static final String NO_RESPONSE = "NO RESPONSE"; 452 453 /** 454 * Calls into the test app to process user data. 455 * Asserts if the user data could not be processed or was version 456 * incompatible with the previously processed user data. 457 */ processUserData(String packageName)458 static void processUserData(String packageName) { 459 Intent intent = new Intent(); 460 intent.setComponent(new ComponentName(packageName, 461 "com.android.tests.rollback.testapp.ProcessUserData")); 462 Context context = InstrumentationRegistry.getContext(); 463 464 HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread"); 465 handlerThread.start(); 466 467 // It can sometimes take a while after rollback before the app will 468 // receive this broadcast, so try a few times in a loop. 469 String result = NO_RESPONSE; 470 for (int i = 0; result.equals(NO_RESPONSE) && i < 5; ++i) { 471 BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>(); 472 context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { 473 @Override 474 public void onReceive(Context context, Intent intent) { 475 if (getResultCode() == 1) { 476 resultQueue.add("OK"); 477 } else { 478 // If the test app doesn't receive the broadcast or 479 // fails to set the result data, then getResultData 480 // here returns the initial NO_RESPONSE data passed to 481 // the sendOrderedBroadcast call. 482 resultQueue.add(getResultData()); 483 } 484 } 485 }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null); 486 487 try { 488 result = resultQueue.take(); 489 } catch (InterruptedException e) { 490 throw new AssertionError(e); 491 } 492 } 493 494 handlerThread.quit(); 495 if (!"OK".equals(result)) { 496 fail(result); 497 } 498 } 499 500 /** 501 * Return the rollback info for a recently committed rollback, by matching the rollback id, or 502 * return null if no matching rollback is found. 503 */ getRecentlyCommittedRollbackInfoById(int getRollbackId)504 static RollbackInfo getRecentlyCommittedRollbackInfoById(int getRollbackId) { 505 for (RollbackInfo info : getRollbackManager().getRecentlyCommittedRollbacks()) { 506 if (info.getRollbackId() == getRollbackId) { 507 return info; 508 } 509 } 510 return null; 511 } 512 513 /** 514 * Send broadcast to crash {@code packageName} {@code count} times. If {@code count} is at least 515 * {@link PackageWatchdog#TRIGGER_FAILURE_COUNT}, watchdog crash detection will be triggered. 516 */ sendCrashBroadcast(Context context, String packageName, int count)517 static BroadcastReceiver sendCrashBroadcast(Context context, String packageName, int count) 518 throws InterruptedException, IOException { 519 BlockingQueue<Integer> crashQueue = new SynchronousQueue<>(); 520 IntentFilter crashCountFilter = new IntentFilter(); 521 crashCountFilter.addAction("com.android.tests.rollback.CRASH"); 522 crashCountFilter.addCategory(Intent.CATEGORY_DEFAULT); 523 524 BroadcastReceiver crashCountReceiver = new BroadcastReceiver() { 525 @Override 526 public void onReceive(Context context, Intent intent) { 527 try { 528 // Sleep long enough for packagewatchdog to be notified of crash 529 Thread.sleep(1000); 530 // Kill app and close AppErrorDialog 531 ActivityManager am = context.getSystemService(ActivityManager.class); 532 am.killBackgroundProcesses(packageName); 533 // Allow another package launch 534 crashQueue.put(intent.getIntExtra("count", 0)); 535 } catch (InterruptedException e) { 536 fail("Failed to communicate with test app"); 537 } 538 } 539 }; 540 context.registerReceiver(crashCountReceiver, crashCountFilter); 541 542 do { 543 launchPackage(packageName); 544 } while(crashQueue.take() < count); 545 return crashCountReceiver; 546 } 547 } 548