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