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.install.lib;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.junit.Assert.fail;
23 
24 import android.app.UiAutomation;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageInstaller;
33 import android.content.pm.PackageManager;
34 import android.os.Handler;
35 import android.os.HandlerThread;
36 import android.os.SystemClock;
37 
38 import androidx.test.InstrumentationRegistry;
39 
40 import com.google.common.annotations.VisibleForTesting;
41 
42 import java.io.IOException;
43 import java.lang.reflect.Field;
44 import java.util.List;
45 import java.util.concurrent.BlockingQueue;
46 import java.util.concurrent.LinkedBlockingQueue;
47 import java.util.concurrent.TimeUnit;
48 
49 /**
50  * Utilities to facilitate installation in tests.
51  */
52 public class InstallUtils {
53     private static final int NUM_MAX_POLLS = 5;
54     private static final int POLL_WAIT_TIME_MILLIS = 200;
55     private static final long GET_UIAUTOMATION_TIMEOUT_MS = 60000;
56 
getUiAutomation()57     private static UiAutomation getUiAutomation() {
58         final long start = SystemClock.uptimeMillis();
59         while (SystemClock.uptimeMillis() - start < GET_UIAUTOMATION_TIMEOUT_MS) {
60             UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
61             if (ui != null) {
62                 return ui;
63             }
64         }
65         throw new AssertionError("Failed to get UiAutomation");
66     }
67 
68     /**
69      * Adopts the given shell permissions.
70      */
adoptShellPermissionIdentity(String... permissions)71     public static void adoptShellPermissionIdentity(String... permissions) {
72         getUiAutomation().adoptShellPermissionIdentity(permissions);
73     }
74 
75     /**
76      * Drops all shell permissions.
77      */
dropShellPermissionIdentity()78     public static void dropShellPermissionIdentity() {
79         getUiAutomation().dropShellPermissionIdentity();
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     public static long getInstalledVersion(String packageName) {
86         Context context = InstrumentationRegistry.getTargetContext();
87         PackageManager pm = context.getPackageManager();
88         try {
89             PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
90             return info.getLongVersionCode();
91         } catch (PackageManager.NameNotFoundException e) {
92             return -1;
93         }
94     }
95 
96     /**
97      * Waits for the given session to be marked as ready or failed and returns it.
98      */
waitForSession(int sessionId)99     public static PackageInstaller.SessionInfo waitForSession(int sessionId) {
100         BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>();
101         BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() {
102             @Override
103             public void onReceive(Context context, Intent intent) {
104                 PackageInstaller.SessionInfo info =
105                         intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
106                 if (info != null && info.getSessionId() == sessionId) {
107                     if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
108                         try {
109                             sessionStatus.put(info);
110                         } catch (InterruptedException e) {
111                             throw new AssertionError(e);
112                         }
113                     }
114                 }
115             }
116         };
117         IntentFilter sessionUpdatedFilter =
118                 new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED);
119 
120         Context context = InstrumentationRegistry.getTargetContext();
121         context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter);
122 
123         PackageInstaller installer = getPackageInstaller();
124         PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId);
125 
126         try {
127             if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
128                 sessionStatus.put(info);
129             }
130             info = sessionStatus.poll(60, TimeUnit.SECONDS);
131             context.unregisterReceiver(sessionUpdatedReceiver);
132             assertWithMessage("Timed out while waiting for session to get ready/failed")
133                     .that(info).isNotNull();
134             assertThat(info.getSessionId()).isEqualTo(sessionId);
135             return info;
136         } catch (InterruptedException e) {
137             throw new AssertionError(e);
138         }
139     }
140 
141     /**
142      * Waits for the given session to be marked as ready.
143      * Throws an assertion if the session fails.
144      */
waitForSessionReady(int sessionId)145     public static void waitForSessionReady(int sessionId) {
146         PackageInstaller.SessionInfo info = waitForSession(sessionId);
147         // TODO: migrate to PackageInstallerSessionInfoSubject
148         if (info.isStagedSessionFailed()) {
149             throw new AssertionError(info.getStagedSessionErrorMessage());
150         }
151     }
152 
153     /**
154      * Returns the info for the given package name.
155      */
getPackageInfo(String packageName)156     public static PackageInfo getPackageInfo(String packageName) {
157         Context context = InstrumentationRegistry.getTargetContext();
158         PackageManager pm = context.getPackageManager();
159         try {
160             return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
161         } catch (PackageManager.NameNotFoundException e) {
162             return null;
163         }
164     }
165 
166     /**
167      * Returns the PackageInstaller instance of the current {@code Context}
168      */
getPackageInstaller()169     public static PackageInstaller getPackageInstaller() {
170         return InstrumentationRegistry.getTargetContext().getPackageManager().getPackageInstaller();
171     }
172 
173     /**
174      * Returns an existing session to actively perform work.
175      * {@see PackageInstaller#openSession}
176      */
openPackageInstallerSession(int sessionId)177     public static PackageInstaller.Session openPackageInstallerSession(int sessionId)
178             throws IOException {
179         return getPackageInstaller().openSession(sessionId);
180     }
181 
182     /**
183      * Asserts that {@code result} intent has a success status.
184      */
assertStatusSuccess(Intent result)185     public static void assertStatusSuccess(Intent result) {
186         int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
187                 PackageInstaller.STATUS_FAILURE);
188         if (status == -1) {
189             throw new AssertionError("PENDING USER ACTION");
190         } else if (status > 0) {
191             String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
192             throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
193         }
194     }
195 
196     /**
197      * Asserts that {@code result} intent has a failure status.
198      */
assertStatusFailure(Intent result)199     public static void assertStatusFailure(Intent result) {
200         // Pass SUCCESS as default to ensure that this doesn't accidentally pass
201         int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
202                 PackageInstaller.STATUS_SUCCESS);
203         switch (status) {
204             case -2: // PackageInstaller.STATUS_PENDING_STREAMING
205             case PackageInstaller.STATUS_PENDING_USER_ACTION:
206                 throw new AssertionError("PENDING " + status);
207             case PackageInstaller.STATUS_SUCCESS:
208                 throw new AssertionError("INCORRECT SUCCESS ");
209             case PackageInstaller.STATUS_FAILURE:
210             case PackageInstaller.STATUS_FAILURE_BLOCKED:
211             case PackageInstaller.STATUS_FAILURE_ABORTED:
212             case PackageInstaller.STATUS_FAILURE_INVALID:
213             case PackageInstaller.STATUS_FAILURE_CONFLICT:
214             case PackageInstaller.STATUS_FAILURE_STORAGE:
215             case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
216                 break;
217             default:
218                 String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
219                 throw new AssertionError(message == null ? "UNKNOWN STATUS" : message);
220         }
221     }
222 
223     /**
224      * Commits {@link Install} but expects to fail.
225      *
226      * @param expectedThrowableClass class or superclass of the expected throwable.
227      *
228      */
commitExpectingFailure(Class expectedThrowableClass, String expectedFailMessage, Install install)229     public static void commitExpectingFailure(Class expectedThrowableClass,
230             String expectedFailMessage, Install install) {
231         assertThrows(expectedThrowableClass, expectedFailMessage, () -> install.commit());
232     }
233 
234     /**
235      * Mutates {@code installFlags} field of {@code params} by adding {@code
236      * additionalInstallFlags} to it.
237      */
238     @VisibleForTesting
mutateInstallFlags(PackageInstaller.SessionParams params, int additionalInstallFlags)239     public static void mutateInstallFlags(PackageInstaller.SessionParams params,
240             int additionalInstallFlags) {
241         final Class<?> clazz = params.getClass();
242         Field installFlagsField;
243         try {
244             installFlagsField = clazz.getDeclaredField("installFlags");
245         } catch (NoSuchFieldException e) {
246             throw new AssertionError("Unable to reflect over SessionParams.installFlags", e);
247         }
248 
249         try {
250             int flags = installFlagsField.getInt(params);
251             flags |= additionalInstallFlags;
252             installFlagsField.setAccessible(true);
253             installFlagsField.setInt(params, flags);
254         } catch (IllegalAccessException e) {
255             throw new AssertionError("Unable to reflect over SessionParams.installFlags", e);
256         }
257     }
258 
259     private static final String NO_RESPONSE = "NO RESPONSE";
260 
261     /**
262      * Calls into the test app to process user data.
263      * Asserts if the user data could not be processed or was version
264      * incompatible with the previously processed user data.
265      */
processUserData(String packageName)266     public static void processUserData(String packageName) {
267         Intent intent = new Intent();
268         intent.setComponent(new ComponentName(packageName,
269                 "com.android.cts.install.lib.testapp.ProcessUserData"));
270         intent.setAction("PROCESS_USER_DATA");
271         Context context = InstrumentationRegistry.getTargetContext();
272 
273         HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
274         handlerThread.start();
275 
276         // It can sometimes take a while after rollback before the app will
277         // receive this broadcast, so try a few times in a loop.
278         String result = NO_RESPONSE;
279         for (int i = 0; i < NUM_MAX_POLLS; ++i) {
280             BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>();
281             context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
282                 @Override
283                 public void onReceive(Context context, Intent intent) {
284                     if (getResultCode() == 1) {
285                         resultQueue.add("OK");
286                     } else {
287                         // If the test app doesn't receive the broadcast or
288                         // fails to set the result data, then getResultData
289                         // here returns the initial NO_RESPONSE data passed to
290                         // the sendOrderedBroadcast call.
291                         resultQueue.add(getResultData());
292                     }
293                 }
294             }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null);
295 
296             try {
297                 result = resultQueue.take();
298                 if (!result.equals(NO_RESPONSE)) {
299                     break;
300                 }
301                 Thread.sleep(POLL_WAIT_TIME_MILLIS);
302             } catch (InterruptedException e) {
303                 throw new AssertionError(e);
304             }
305         }
306 
307         assertThat(result).isEqualTo("OK");
308     }
309 
310     /**
311      * Retrieves the app's user data version from userdata.txt.
312      * @return -1 if userdata.txt doesn't exist or -2 if the app doesn't handle the broadcast which
313      * could happen when the app crashes or doesn't start at all.
314      */
getUserDataVersion(String packageName)315     public static int getUserDataVersion(String packageName) {
316         Intent intent = new Intent();
317         intent.setComponent(new ComponentName(packageName,
318                 "com.android.cts.install.lib.testapp.ProcessUserData"));
319         intent.setAction("GET_USER_DATA_VERSION");
320         Context context = InstrumentationRegistry.getTargetContext();
321 
322         HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
323         handlerThread.start();
324 
325         // The response code returned when the broadcast is not received by the app or when the app
326         // crashes during handling the broadcast. We will retry when this code is returned.
327         final int noResponse = -2;
328         // It can sometimes take a while after rollback before the app will
329         // receive this broadcast, so try a few times in a loop.
330         BlockingQueue<Integer> resultQueue = new LinkedBlockingQueue<>();
331         for (int i = 0; i < NUM_MAX_POLLS; ++i) {
332             context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
333                 @Override
334                 public void onReceive(Context context, Intent intent) {
335                     resultQueue.add(getResultCode());
336                 }
337             }, new Handler(handlerThread.getLooper()), noResponse, null, null);
338 
339             try {
340                 int userDataVersion = resultQueue.take();
341                 if (userDataVersion != noResponse) {
342                     return userDataVersion;
343                 }
344                 Thread.sleep(POLL_WAIT_TIME_MILLIS);
345             } catch (InterruptedException e) {
346                 throw new AssertionError(e);
347             }
348         }
349 
350         return noResponse;
351     }
352 
353     /**
354      * Checks whether the given package is installed on /system and was not updated.
355      */
isSystemAppWithoutUpdate(String packageName)356     static boolean isSystemAppWithoutUpdate(String packageName) {
357         PackageInfo pi = getPackageInfo(packageName);
358         if (pi == null) {
359             return false;
360         } else {
361             return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
362                     && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0);
363         }
364     }
365 
366     /**
367      * Checks whether a given package is installed for only the given user, from a list of users.
368      * @param packageName the package to check
369      * @param userIdToCheck the user id of the user to check
370      * @param userIds a list of user ids to check
371      * @return {@code true} if the package is only installed for the given user,
372      *         {@code false} otherwise.
373      */
isOnlyInstalledForUser(String packageName, int userIdToCheck, List<Integer> userIds)374     public static boolean isOnlyInstalledForUser(String packageName, int userIdToCheck,
375             List<Integer> userIds) {
376         Context context = InstrumentationRegistry.getTargetContext();
377         PackageManager pm = context.getPackageManager();
378         for (int userId: userIds) {
379             List<PackageInfo> installedPackages;
380             if (userId != userIdToCheck) {
381                 installedPackages = pm.getInstalledPackagesAsUser(PackageManager.MATCH_APEX,
382                         userId);
383                 for (PackageInfo pi : installedPackages) {
384                     if (pi.packageName.equals(packageName)) {
385                         return false;
386                     }
387                 }
388             }
389         }
390         return true;
391     }
392 
393     /**
394      * Returns the session by session Id, or null if no session is found.
395      */
getStagedSessionInfo(int sessionId)396     public static PackageInstaller.SessionInfo getStagedSessionInfo(int sessionId) {
397         PackageInstaller packageInstaller = getPackageInstaller();
398         for (PackageInstaller.SessionInfo session : packageInstaller.getStagedSessions()) {
399             if (session.getSessionId() == sessionId) {
400                 return session;
401             }
402         }
403         return null;
404     }
405 
406     /**
407      * Assert that the given staged session is abandoned. The method assumes that the given session
408      * is staged.
409      * @param sessionId of the staged session
410      */
assertStagedSessionIsAbandoned(int sessionId)411     public static void assertStagedSessionIsAbandoned(int sessionId) {
412         assertThat(getStagedSessionInfo(sessionId)).isNull();
413     }
414 
415     /**
416      * A functional interface representing an operation that takes no arguments,
417      * returns no arguments and might throw a {@link Throwable} of any kind.
418      */
419     @FunctionalInterface
420     private interface Operation {
421         /**
422          * This is the method that gets called for any object that implements this interface.
423          */
run()424         void run() throws Throwable;
425     }
426 
427     /**
428      * Runs {@link Operation} and expects a {@link Throwable} of the given class to be thrown.
429      *
430      * @param expectedThrowableClass class or superclass of the expected throwable.
431      */
assertThrows(Class expectedThrowableClass, String expectedFailMessage, Operation operation)432     private static void assertThrows(Class expectedThrowableClass, String expectedFailMessage,
433             Operation operation) {
434         try {
435             operation.run();
436         } catch (Throwable expected) {
437             assertThat(expectedThrowableClass.isAssignableFrom(expected.getClass())).isTrue();
438             assertThat(expected.getMessage()).containsMatch(expectedFailMessage);
439             return;
440         }
441         fail("Operation was expected to fail!");
442     }
443 }
444