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.rollback.lib; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import android.app.ActivityManager; 22 import android.app.AlarmManager; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.pm.VersionedPackage; 28 import android.content.rollback.PackageRollbackInfo; 29 import android.content.rollback.RollbackInfo; 30 import android.content.rollback.RollbackManager; 31 import android.util.Log; 32 33 import androidx.test.InstrumentationRegistry; 34 35 import com.android.cts.install.lib.LocalIntentSender; 36 import com.android.cts.install.lib.TestApp; 37 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Objects; 42 import java.util.concurrent.CountDownLatch; 43 import java.util.function.Predicate; 44 import java.util.function.Supplier; 45 46 /** 47 * Utilities to facilitate testing rollbacks. 48 */ 49 public class RollbackUtils { 50 51 private static final String TAG = "RollbackTest"; 52 53 /** 54 * Time between repeated checks in {@link #retry}. 55 */ 56 private static final long RETRY_CHECK_INTERVAL_MILLIS = 500; 57 58 /** 59 * Maximum number of checks in {@link #retry} before a timeout occurs. 60 */ 61 private static final long RETRY_MAX_INTERVALS = 20; 62 63 64 /** 65 * Gets the RollbackManager for the instrumentation context. 66 */ getRollbackManager()67 public static RollbackManager getRollbackManager() { 68 Context context = InstrumentationRegistry.getContext(); 69 RollbackManager rm = (RollbackManager) context.getSystemService(Context.ROLLBACK_SERVICE); 70 if (rm == null) { 71 throw new AssertionError("Failed to get RollbackManager"); 72 } 73 return rm; 74 } 75 76 /** 77 * Returns a rollback for the given rollback Id, if found. Otherwise, returns null. 78 */ getRollbackById(List<RollbackInfo> rollbacks, int rollbackId)79 private static RollbackInfo getRollbackById(List<RollbackInfo> rollbacks, int rollbackId) { 80 for (RollbackInfo rollback :rollbacks) { 81 if (rollback.getRollbackId() == rollbackId) { 82 return rollback; 83 } 84 } 85 return null; 86 } 87 88 /** 89 * Returns an available rollback for the given package name. Returns null 90 * if there are no available rollbacks, and throws an assertion if there 91 * is more than one. 92 */ getAvailableRollback(String packageName)93 public static RollbackInfo getAvailableRollback(String packageName) { 94 RollbackManager rm = getRollbackManager(); 95 return getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), packageName); 96 } 97 98 /** 99 * Returns a recently committed rollback for the given package name. Returns null 100 * if there are no available rollbacks, and throws an assertion if there 101 * is more than one. 102 */ getCommittedRollback(String packageName)103 public static RollbackInfo getCommittedRollback(String packageName) { 104 RollbackManager rm = getRollbackManager(); 105 return getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), packageName); 106 } 107 108 /** 109 * Returns a recently committed rollback for the given rollback Id. 110 * Returns null if no committed rollback with a matching Id was found. 111 */ getCommittedRollbackById(int rollbackId)112 public static RollbackInfo getCommittedRollbackById(int rollbackId) { 113 RollbackManager rm = getRollbackManager(); 114 return getRollbackById(rm.getRecentlyCommittedRollbacks(), rollbackId); 115 } 116 117 /** 118 * Commit the given rollback. This method won't return until the committed session is made 119 * ready or failed. The caller is safe to immediately reboot the device right after the call. 120 * @throws AssertionError if the rollback fails. 121 */ rollback(int rollbackId, TestApp... causePackages)122 public static void rollback(int rollbackId, TestApp... causePackages) 123 throws InterruptedException { 124 List<VersionedPackage> causes = new ArrayList<>(); 125 for (TestApp cause : causePackages) { 126 causes.add(cause.getVersionedPackage()); 127 } 128 129 RollbackManager rm = getRollbackManager(); 130 LocalIntentSender sender = new LocalIntentSender(); 131 rm.commitRollback(rollbackId, causes, sender.getIntentSender()); 132 Intent result = sender.getResult(); 133 int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, 134 RollbackManager.STATUS_FAILURE); 135 if (status != RollbackManager.STATUS_SUCCESS) { 136 String message = result.getStringExtra(RollbackManager.EXTRA_STATUS_MESSAGE); 137 throw new AssertionError(message); 138 } 139 } 140 141 /** 142 * Forwards the device clock time by {@code offsetMillis}. 143 */ forwardTimeBy(long offsetMillis)144 public static void forwardTimeBy(long offsetMillis) { 145 setTime(System.currentTimeMillis() + offsetMillis); 146 Log.i(TAG, "Forwarded time on device by " + offsetMillis + " millis"); 147 } 148 149 /** 150 * Returns the RollbackInfo with a given package in the list of rollbacks. 151 * Throws an assertion failure if there is more than one such rollback 152 * info. Returns null if there are no such rollback infos. 153 */ getUniqueRollbackInfoForPackage(List<RollbackInfo> rollbacks, String packageName)154 public static RollbackInfo getUniqueRollbackInfoForPackage(List<RollbackInfo> rollbacks, 155 String packageName) { 156 RollbackInfo found = null; 157 for (RollbackInfo rollback : rollbacks) { 158 for (PackageRollbackInfo info : rollback.getPackages()) { 159 if (packageName.equals(info.getPackageName())) { 160 assertThat(found).isNull(); 161 found = rollback; 162 break; 163 } 164 } 165 } 166 return found; 167 } 168 169 /** 170 * Returns an available rollback matching the specified package name. If no such rollback is 171 * available, getAvailableRollbacks is called repeatedly until one becomes available. An 172 * assertion is raised if this does not occur after a certain number of checks. 173 */ waitForAvailableRollback(String packageName)174 public static RollbackInfo waitForAvailableRollback(String packageName) 175 throws InterruptedException { 176 return retry(() -> getAvailableRollback(packageName), 177 Objects::nonNull, "Rollback did not become available."); 178 } 179 180 /** 181 * If there is no available rollback matching the specified package name, this returns 182 * immediately. If such a rollback is available, getAvailableRollbacks is called repeatedly 183 * until it is no longer available. An assertion is raised if this does not occur after a 184 * certain number of checks. 185 */ waitForUnavailableRollback(String packageName)186 public static void waitForUnavailableRollback(String packageName) throws InterruptedException { 187 retry(() -> getAvailableRollback(packageName), Objects::isNull, 188 "Rollback did not become unavailable"); 189 } 190 hasRollbackInclude(List<RollbackInfo> rollbacks, String packageName)191 private static boolean hasRollbackInclude(List<RollbackInfo> rollbacks, String packageName) { 192 return rollbacks.stream().anyMatch( 193 ri -> ri.getPackages().stream().anyMatch( 194 pri -> packageName.equals(pri.getPackageName()))); 195 } 196 197 /** 198 * Retries until all rollbacks including {@code packageName} are gone. An assertion is raised if 199 * this does not occur after a certain number of checks. 200 */ waitForRollbackGone( Supplier<List<RollbackInfo>> supplier, String packageName)201 public static void waitForRollbackGone( 202 Supplier<List<RollbackInfo>> supplier, String packageName) throws InterruptedException { 203 retry(supplier, rollbacks -> !hasRollbackInclude(rollbacks, packageName), 204 "Rollback containing " + packageName + " did not go away"); 205 } 206 retry(Supplier<T> supplier, Predicate<T> predicate, String message)207 private static <T> T retry(Supplier<T> supplier, Predicate<T> predicate, String message) 208 throws InterruptedException { 209 for (int i = 0; i < RETRY_MAX_INTERVALS; i++) { 210 T result = supplier.get(); 211 if (predicate.test(result)) { 212 return result; 213 } 214 Thread.sleep(RETRY_CHECK_INTERVAL_MILLIS); 215 } 216 throw new AssertionError(message); 217 } 218 219 /** 220 * Send broadcast to crash {@code packageName} {@code count} times. If {@code count} is at least 221 * {@link PackageWatchdog#TRIGGER_FAILURE_COUNT}, watchdog crash detection will be triggered. 222 */ sendCrashBroadcast(String packageName, int count)223 public static void sendCrashBroadcast(String packageName, 224 int count) throws InterruptedException, IOException { 225 for (int i = 0; i < count; ++i) { 226 launchPackageForCrash(packageName); 227 } 228 } 229 setTime(long millis)230 private static void setTime(long millis) { 231 Context context = InstrumentationRegistry.getContext(); 232 AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 233 am.setTime(millis); 234 } 235 236 /** 237 * Launches {@code packageName} with {@link Intent#ACTION_MAIN} and 238 * waits for a CRASH broadcast from the launched app. 239 */ launchPackageForCrash(String packageName)240 private static void launchPackageForCrash(String packageName) 241 throws InterruptedException, IOException { 242 // Force stop the package before launching it to make sure it isn't 243 // stuck in a non-launchable state. And wait a second afterwards to 244 // avoid interfering with when we launch the app. 245 Log.i(TAG, "Force stopping " + packageName); 246 Context context = InstrumentationRegistry.getContext(); 247 ActivityManager am = context.getSystemService(ActivityManager.class); 248 am.forceStopPackage(packageName); 249 Thread.sleep(1000); 250 251 // Register a receiver to listen for the CRASH broadcast. 252 CountDownLatch latch = new CountDownLatch(1); 253 IntentFilter crashFilter = new IntentFilter(); 254 crashFilter.addAction("com.android.tests.rollback.CRASH"); 255 crashFilter.addCategory(Intent.CATEGORY_DEFAULT); 256 BroadcastReceiver crashReceiver = new BroadcastReceiver() { 257 @Override 258 public void onReceive(Context context, Intent intent) { 259 Log.i(TAG, "Received CRASH broadcast from " + packageName); 260 latch.countDown(); 261 } 262 }; 263 context.registerReceiver(crashReceiver, crashFilter, 264 Context.RECEIVER_EXPORTED_UNAUDITED); 265 266 // Launch the app. 267 Intent intent = new Intent(Intent.ACTION_MAIN); 268 intent.setPackage(packageName); 269 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 270 intent.addCategory(Intent.CATEGORY_LAUNCHER); 271 Log.i(TAG, "Launching " + packageName + " with " + intent); 272 context.startActivity(intent); 273 274 Log.i(TAG, "Waiting for CRASH broadcast from " + packageName); 275 latch.await(); 276 277 context.unregisterReceiver(crashReceiver); 278 279 // Sleep long enough for packagewatchdog to be notified of crash 280 Thread.sleep(1000); 281 } 282 } 283 284