blob: 2a64efede91ab4253a0d0a4fd79f4aa375afdb7f [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.install.lib;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.app.UiAutomation;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import androidx.test.InstrumentationRegistry;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Utilities to facilitate installation in tests.
*/
public class InstallUtils {
private static final int NUM_MAX_POLLS = 5;
private static final int POLL_WAIT_TIME_MILLIS = 200;
private static final long GET_UIAUTOMATION_TIMEOUT_MS = 60000;
private static UiAutomation getUiAutomation() {
final long start = SystemClock.uptimeMillis();
while (SystemClock.uptimeMillis() - start < GET_UIAUTOMATION_TIMEOUT_MS) {
UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
if (ui != null) {
return ui;
}
}
throw new AssertionError("Failed to get UiAutomation");
}
/**
* Adopts the given shell permissions.
*/
public static void adoptShellPermissionIdentity(String... permissions) {
getUiAutomation().adoptShellPermissionIdentity(permissions);
}
/**
* Drops all shell permissions.
*/
public static void dropShellPermissionIdentity() {
getUiAutomation().dropShellPermissionIdentity();
}
/**
* Returns the version of the given package installed on device.
* Returns -1 if the package is not currently installed.
*/
public static long getInstalledVersion(String packageName) {
Context context = InstrumentationRegistry.getTargetContext();
PackageManager pm = context.getPackageManager();
try {
PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
return info.getLongVersionCode();
} catch (PackageManager.NameNotFoundException e) {
return -1;
}
}
/**
* Waits for the given session to be marked as ready or failed and returns it.
*/
public static PackageInstaller.SessionInfo waitForSession(int sessionId) {
BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>();
BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
PackageInstaller.SessionInfo info =
intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
if (info != null && info.getSessionId() == sessionId) {
if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
try {
sessionStatus.put(info);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}
}
};
IntentFilter sessionUpdatedFilter =
new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED);
Context context = InstrumentationRegistry.getTargetContext();
context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter);
PackageInstaller installer = getPackageInstaller();
PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId);
try {
if (info.isStagedSessionReady() || info.isStagedSessionFailed()) {
sessionStatus.put(info);
}
info = sessionStatus.poll(60, TimeUnit.SECONDS);
context.unregisterReceiver(sessionUpdatedReceiver);
assertWithMessage("Timed out while waiting for session to get ready/failed")
.that(info).isNotNull();
assertThat(info.getSessionId()).isEqualTo(sessionId);
return info;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
/**
* Waits for the given session to be marked as ready.
* Throws an assertion if the session fails.
*/
public static void waitForSessionReady(int sessionId) {
PackageInstaller.SessionInfo info = waitForSession(sessionId);
// TODO: migrate to PackageInstallerSessionInfoSubject
if (info.isStagedSessionFailed()) {
throw new AssertionError(info.getStagedSessionErrorMessage());
}
}
/**
* Returns the info for the given package name.
*/
public static PackageInfo getPackageInfo(String packageName) {
Context context = InstrumentationRegistry.getTargetContext();
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
/**
* Returns the PackageInstaller instance of the current {@code Context}
*/
public static PackageInstaller getPackageInstaller() {
return InstrumentationRegistry.getTargetContext().getPackageManager().getPackageInstaller();
}
/**
* Returns an existing session to actively perform work.
* {@see PackageInstaller#openSession}
*/
public static PackageInstaller.Session openPackageInstallerSession(int sessionId)
throws IOException {
return getPackageInstaller().openSession(sessionId);
}
/**
* Asserts that {@code result} intent has a success status.
*/
public static void assertStatusSuccess(Intent result) {
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
if (status == -1) {
throw new AssertionError("PENDING USER ACTION");
} else if (status > 0) {
String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message);
}
}
/**
* Asserts that {@code result} intent has a failure status.
*/
public static void assertStatusFailure(Intent result) {
// Pass SUCCESS as default to ensure that this doesn't accidentally pass
int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_SUCCESS);
switch (status) {
case -2: // PackageInstaller.STATUS_PENDING_STREAMING
case PackageInstaller.STATUS_PENDING_USER_ACTION:
throw new AssertionError("PENDING " + status);
case PackageInstaller.STATUS_SUCCESS:
throw new AssertionError("INCORRECT SUCCESS ");
case PackageInstaller.STATUS_FAILURE:
case PackageInstaller.STATUS_FAILURE_BLOCKED:
case PackageInstaller.STATUS_FAILURE_ABORTED:
case PackageInstaller.STATUS_FAILURE_INVALID:
case PackageInstaller.STATUS_FAILURE_CONFLICT:
case PackageInstaller.STATUS_FAILURE_STORAGE:
case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
break;
default:
String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
throw new AssertionError(message == null ? "UNKNOWN STATUS" : message);
}
}
/**
* Commits {@link Install} but expects to fail.
*
* @param expectedThrowableClass class or superclass of the expected throwable.
*
*/
public static void commitExpectingFailure(Class expectedThrowableClass,
String expectedFailMessage, Install install) {
assertThrows(expectedThrowableClass, expectedFailMessage, () -> install.commit());
}
/**
* Mutates {@code installFlags} field of {@code params} by adding {@code
* additionalInstallFlags} to it.
*/
@VisibleForTesting
public static void mutateInstallFlags(PackageInstaller.SessionParams params,
int additionalInstallFlags) {
final Class<?> clazz = params.getClass();
Field installFlagsField;
try {
installFlagsField = clazz.getDeclaredField("installFlags");
} catch (NoSuchFieldException e) {
throw new AssertionError("Unable to reflect over SessionParams.installFlags", e);
}
try {
int flags = installFlagsField.getInt(params);
flags |= additionalInstallFlags;
installFlagsField.setAccessible(true);
installFlagsField.setInt(params, flags);
} catch (IllegalAccessException e) {
throw new AssertionError("Unable to reflect over SessionParams.installFlags", e);
}
}
private static final String NO_RESPONSE = "NO RESPONSE";
/**
* Calls into the test app to process user data.
* Asserts if the user data could not be processed or was version
* incompatible with the previously processed user data.
*/
public static void processUserData(String packageName) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName,
"com.android.cts.install.lib.testapp.ProcessUserData"));
intent.setAction("PROCESS_USER_DATA");
Context context = InstrumentationRegistry.getTargetContext();
HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
handlerThread.start();
// It can sometimes take a while after rollback before the app will
// receive this broadcast, so try a few times in a loop.
String result = NO_RESPONSE;
for (int i = 0; i < NUM_MAX_POLLS; ++i) {
BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>();
context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (getResultCode() == 1) {
resultQueue.add("OK");
} else {
// If the test app doesn't receive the broadcast or
// fails to set the result data, then getResultData
// here returns the initial NO_RESPONSE data passed to
// the sendOrderedBroadcast call.
resultQueue.add(getResultData());
}
}
}, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null);
try {
result = resultQueue.take();
if (!result.equals(NO_RESPONSE)) {
break;
}
Thread.sleep(POLL_WAIT_TIME_MILLIS);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
assertThat(result).isEqualTo("OK");
}
/**
* Retrieves the app's user data version from userdata.txt.
* @return -1 if userdata.txt doesn't exist or -2 if the app doesn't handle the broadcast which
* could happen when the app crashes or doesn't start at all.
*/
public static int getUserDataVersion(String packageName) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(packageName,
"com.android.cts.install.lib.testapp.ProcessUserData"));
intent.setAction("GET_USER_DATA_VERSION");
Context context = InstrumentationRegistry.getTargetContext();
HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread");
handlerThread.start();
// The response code returned when the broadcast is not received by the app or when the app
// crashes during handling the broadcast. We will retry when this code is returned.
final int noResponse = -2;
// It can sometimes take a while after rollback before the app will
// receive this broadcast, so try a few times in a loop.
BlockingQueue<Integer> resultQueue = new LinkedBlockingQueue<>();
for (int i = 0; i < NUM_MAX_POLLS; ++i) {
context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
resultQueue.add(getResultCode());
}
}, new Handler(handlerThread.getLooper()), noResponse, null, null);
try {
int userDataVersion = resultQueue.take();
if (userDataVersion != noResponse) {
return userDataVersion;
}
Thread.sleep(POLL_WAIT_TIME_MILLIS);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
return noResponse;
}
/**
* Checks whether the given package is installed on /system and was not updated.
*/
static boolean isSystemAppWithoutUpdate(String packageName) {
PackageInfo pi = getPackageInfo(packageName);
if (pi == null) {
return false;
} else {
return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
&& ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0);
}
}
/**
* Checks whether a given package is installed for only the given user, from a list of users.
* @param packageName the package to check
* @param userIdToCheck the user id of the user to check
* @param userIds a list of user ids to check
* @return {@code true} if the package is only installed for the given user,
* {@code false} otherwise.
*/
public static boolean isOnlyInstalledForUser(String packageName, int userIdToCheck,
List<Integer> userIds) {
Context context = InstrumentationRegistry.getTargetContext();
PackageManager pm = context.getPackageManager();
for (int userId: userIds) {
List<PackageInfo> installedPackages;
if (userId != userIdToCheck) {
installedPackages = pm.getInstalledPackagesAsUser(PackageManager.MATCH_APEX,
userId);
for (PackageInfo pi : installedPackages) {
if (pi.packageName.equals(packageName)) {
return false;
}
}
}
}
return true;
}
/**
* Returns the session by session Id, or null if no session is found.
*/
public static PackageInstaller.SessionInfo getStagedSessionInfo(int sessionId) {
PackageInstaller packageInstaller = getPackageInstaller();
for (PackageInstaller.SessionInfo session : packageInstaller.getStagedSessions()) {
if (session.getSessionId() == sessionId) {
return session;
}
}
return null;
}
/**
* Assert that the given staged session is abandoned. The method assumes that the given session
* is staged.
* @param sessionId of the staged session
*/
public static void assertStagedSessionIsAbandoned(int sessionId) {
assertThat(getStagedSessionInfo(sessionId)).isNull();
}
/**
* A functional interface representing an operation that takes no arguments,
* returns no arguments and might throw a {@link Throwable} of any kind.
*/
@FunctionalInterface
private interface Operation {
/**
* This is the method that gets called for any object that implements this interface.
*/
void run() throws Throwable;
}
/**
* Runs {@link Operation} and expects a {@link Throwable} of the given class to be thrown.
*
* @param expectedThrowableClass class or superclass of the expected throwable.
*/
private static void assertThrows(Class expectedThrowableClass, String expectedFailMessage,
Operation operation) {
try {
operation.run();
} catch (Throwable expected) {
assertThat(expectedThrowableClass.isAssignableFrom(expected.getClass())).isTrue();
assertThat(expected.getMessage()).containsMatch(expectedFailMessage);
return;
}
fail("Operation was expected to fail!");
}
}