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.content.BroadcastReceiver;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
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 
35 import androidx.test.InstrumentationRegistry;
36 
37 import com.google.common.annotations.VisibleForTesting;
38 
39 import java.io.IOException;
40 import java.lang.reflect.Field;
41 import java.util.concurrent.BlockingQueue;
42 import java.util.concurrent.LinkedBlockingQueue;
43 
44 /**
45  * Utilities to facilitate installation in tests.
46  */
47 public class InstallUtils {
48     /**
49      * Adopts the given shell permissions.
50      */
adoptShellPermissionIdentity(String... permissions)51     public static void adoptShellPermissionIdentity(String... permissions) {
52         InstrumentationRegistry
53                 .getInstrumentation()
54                 .getUiAutomation()
55                 .adoptShellPermissionIdentity(permissions);
56     }
57 
58     /**
59      * Drops all shell permissions.
60      */
dropShellPermissionIdentity()61     public static void dropShellPermissionIdentity() {
62         InstrumentationRegistry
63                 .getInstrumentation()
64                 .getUiAutomation()
65                 .dropShellPermissionIdentity();
66     }
67     /**
68      * Returns the version of the given package installed on device.
69      * Returns -1 if the package is not currently installed.
70      */
getInstalledVersion(String packageName)71     public static long getInstalledVersion(String packageName) {
72         Context context = InstrumentationRegistry.getContext();
73         PackageManager pm = context.getPackageManager();
74         try {
75             PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
76             return info.getLongVersionCode();
77         } catch (PackageManager.NameNotFoundException e) {
78             return -1;
79         }
80     }
81 
82     /**
83      * Waits for the given session to be marked as ready.
84      * Throws an assertion if the session fails.
85      */
waitForSessionReady(int sessionId)86     public static void waitForSessionReady(int sessionId) {
87         BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>();
88         BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() {
89             @Override
90             public void onReceive(Context context, Intent intent) {
91                 PackageInstaller.SessionInfo info =
92                         intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
93                 if (info != null && info.getSessionId() == sessionId) {
94                     if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
95                         try {
96                             sessionStatus.put(info);
97                         } catch (InterruptedException e) {
98                             throw new AssertionError(e);
99                         }
100                     }
101                 }
102             }
103         };
104         IntentFilter sessionUpdatedFilter =
105                 new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED);
106 
107         Context context = InstrumentationRegistry.getContext();
108         context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter);
109 
110         PackageInstaller installer = getPackageInstaller();
111         PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId);
112 
113         try {
114             if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
115                 sessionStatus.put(info);
116             }
117 
118             info = sessionStatus.take();
119             context.unregisterReceiver(sessionUpdatedReceiver);
120             if (info.isStagedSessionFailed()) {
121                 throw new AssertionError(info.getStagedSessionErrorMessage());
122             }
123         } catch (InterruptedException e) {
124             throw new AssertionError(e);
125         }
126     }
127 
128     /**
129      * Returns the info for the given package name.
130      */
getPackageInfo(String packageName)131     public static PackageInfo getPackageInfo(String packageName) {
132         Context context = InstrumentationRegistry.getContext();
133         PackageManager pm = context.getPackageManager();
134         try {
135             return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
136         } catch (PackageManager.NameNotFoundException e) {
137             return null;
138         }
139     }
140 
141     /**
142      * Returns the PackageInstaller instance of the current {@code Context}
143      */
getPackageInstaller()144     public static PackageInstaller getPackageInstaller() {
145         return InstrumentationRegistry.getContext().getPackageManager().getPackageInstaller();
146     }
147 
148     /**
149      * Returns an existing session to actively perform work.
150      * {@see PackageInstaller#openSession}
151      */
openPackageInstallerSession(int sessionId)152     public static PackageInstaller.Session openPackageInstallerSession(int sessionId)
153             throws IOException {
154         return getPackageInstaller().openSession(sessionId);
155     }
156 
157     /**
158      * Asserts that {@code result} intent has a success status.
159      */
assertStatusSuccess(Intent result)160     public static void assertStatusSuccess(Intent result) {
161         int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
162                 PackageInstaller.STATUS_FAILURE);
163         if (status == -1) {
164             throw new AssertionError("PENDING USER ACTION");
165         } else if (status > 0) {
166             String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
167             throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
168         }
169     }
170 
171     /**
172      * Commits {@link Install} but expects to fail.
173      *
174      * @param expectedThrowableClass class or superclass of the expected throwable.
175      *
176      */
commitExpectingFailure(Class expectedThrowableClass, String expectedFailMessage, Install install)177     public static void commitExpectingFailure(Class expectedThrowableClass,
178             String expectedFailMessage, Install install) {
179         assertThrows(expectedThrowableClass, expectedFailMessage, () -> install.commit());
180     }
181 
182     /**
183      * Mutates {@code installFlags} field of {@code params} by adding {@code
184      * additionalInstallFlags} to it.
185      */
186     @VisibleForTesting
mutateInstallFlags(PackageInstaller.SessionParams params, int additionalInstallFlags)187     public static void mutateInstallFlags(PackageInstaller.SessionParams params,
188             int additionalInstallFlags) {
189         final Class<?> clazz = params.getClass();
190         Field installFlagsField;
191         try {
192             installFlagsField = clazz.getDeclaredField("installFlags");
193         } catch (NoSuchFieldException e) {
194             throw new AssertionError("Unable to reflect over SessionParams.installFlags", e);
195         }
196 
197         try {
198             int flags = installFlagsField.getInt(params);
199             flags |= additionalInstallFlags;
200             installFlagsField.setAccessible(true);
201             installFlagsField.setInt(params, flags);
202         } catch (IllegalAccessException e) {
203             throw new AssertionError("Unable to reflect over SessionParams.installFlags", e);
204         }
205     }
206 
207     private static final String NO_RESPONSE = "NO RESPONSE";
208 
209     /**
210      * Calls into the test app to process user data.
211      * Asserts if the user data could not be processed or was version
212      * incompatible with the previously processed user data.
213      */
processUserData(String packageName)214     public static void processUserData(String packageName) {
215         Intent intent = new Intent();
216         intent.setComponent(new ComponentName(packageName,
217                 "com.android.cts.install.lib.testapp.ProcessUserData"));
218         Context context = InstrumentationRegistry.getContext();
219 
220         HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
221         handlerThread.start();
222 
223         // It can sometimes take a while after rollback before the app will
224         // receive this broadcast, so try a few times in a loop.
225         String result = NO_RESPONSE;
226         for (int i = 0; result.equals(NO_RESPONSE) && i < 5; ++i) {
227             BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>();
228             context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
229                 @Override
230                 public void onReceive(Context context, Intent intent) {
231                     if (getResultCode() == 1) {
232                         resultQueue.add("OK");
233                     } else {
234                         // If the test app doesn't receive the broadcast or
235                         // fails to set the result data, then getResultData
236                         // here returns the initial NO_RESPONSE data passed to
237                         // the sendOrderedBroadcast call.
238                         resultQueue.add(getResultData());
239                     }
240                 }
241             }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null);
242 
243             try {
244                 result = resultQueue.take();
245             } catch (InterruptedException e) {
246                 throw new AssertionError(e);
247             }
248         }
249 
250         assertThat(result).isEqualTo("OK");
251     }
252 
253     /**
254      * Checks whether the given package is installed on /system and was not updated.
255      */
isSystemAppWithoutUpdate(String packageName)256     static boolean isSystemAppWithoutUpdate(String packageName) {
257         PackageInfo pi = getPackageInfo(packageName);
258         if (pi == null) {
259             return false;
260         } else {
261             return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
262                     && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0);
263         }
264     }
265 
266     /**
267      * A functional interface representing an operation that takes no arguments,
268      * returns no arguments and might throw a {@link Throwable} of any kind.
269      */
270     @FunctionalInterface
271     private interface Operation {
272         /**
273          * This is the method that gets called for any object that implements this interface.
274          */
run()275         void run() throws Throwable;
276     }
277 
278     /**
279      * Runs {@link Operation} and expects a {@link Throwable} of the given class to be thrown.
280      *
281      * @param expectedThrowableClass class or superclass of the expected throwable.
282      */
assertThrows(Class expectedThrowableClass, String expectedFailMessage, Operation operation)283     private static void assertThrows(Class expectedThrowableClass, String expectedFailMessage,
284             Operation operation) {
285         try {
286             operation.run();
287         } catch (Throwable expected) {
288             assertThat(expectedThrowableClass.isAssignableFrom(expected.getClass())).isTrue();
289             assertThat(expected.getMessage()).containsMatch(expectedFailMessage);
290             return;
291         }
292         fail("Operation was expected to fail!");
293     }
294 }
295