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