• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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