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