blob: 4d7274752d83fb4dc001ba3351ea47a9cce7f544 [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.tests.silentupdate;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.content.Context.MODE_PRIVATE;
import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED;
import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_REQUIRED;
import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED;
import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
import static org.junit.Assert.fail;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.os.SystemClock;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* Tests for multi-package (a.k.a. atomic) installs.
*/
@RunWith(JUnit4.class)
public class SilentUpdateTests {
private static final String CURRENT_APK = "SilentInstallCurrent.apk";
private static final String P_APK = "SilentInstallP.apk";
private static final String Q_APK = "SilentInstallQ.apk";
private static final String INSTALLER_PACKAGE_NAME = "com.android.tests.silentupdate";
static final long SILENT_UPDATE_THROTTLE_TIME_SECOND = 10;
private static Context getContext() {
return InstrumentationRegistry.getInstrumentation().getContext();
}
@After
public void tearDown() {
resetSilentUpdatesPolicy();
}
@Test
public void newInstall_RequiresUserAction() throws Exception {
Assert.assertEquals("New install should require user action",
PackageInstaller.STATUS_PENDING_USER_ACTION,
silentInstallResource(CURRENT_APK));
}
@Test
public void updateWithUnknownSourcesDisabled_RequiresUserAction() throws Exception {
Assert.assertEquals("Update with unknown sources disabled should require user action",
PackageInstaller.STATUS_PENDING_USER_ACTION,
silentInstallResource(CURRENT_APK));
}
@Test
public void updateAsNonInstallerOfRecord_RequiresUserAction() throws Exception {
Assert.assertEquals("Update when not installer of record should require user action",
PackageInstaller.STATUS_PENDING_USER_ACTION,
silentInstallResource(CURRENT_APK));
}
@Test
public void updatedInstall_RequiresNoUserAction() throws Exception {
Assert.assertEquals("Nominal silent update should not require user action",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(CURRENT_APK));
}
@Test
public void updatedInstallWithoutCallSetUserAction_RequiresUserAction() throws Exception {
Assert.assertEquals("Update should require action when setRequireUserAction not called",
PackageInstaller.STATUS_PENDING_USER_ACTION,
installResource(CURRENT_APK, null /*setRequireUserAction*/));
}
@Test
public void updatedInstallForceUserAction_RequiresUserAction() throws Exception {
Assert.assertEquals("Update should require action when setRequireUserAction true",
PackageInstaller.STATUS_PENDING_USER_ACTION,
installResource(CURRENT_APK, true /*setRequireUserAction*/));
}
@Test
public void updatePreQApp_RequiresUserAction() throws Exception {
Assert.assertEquals("Updating to a pre-Q app should require user action",
PackageInstaller.STATUS_PENDING_USER_ACTION,
silentInstallResource(P_APK));
}
@Test
public void updateQApp_RequiresNoUserAction() throws Exception {
Assert.assertEquals("Updating to a Q app should not require user action",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(Q_APK));
}
@Test
public void selfUpdate_RequiresNoUserAction() throws Exception {
// persist the last update time to disk
final long lastUpdateTime = getLastUpdateTime();
getContext().getSharedPreferences("default", MODE_PRIVATE)
.edit()
.putLong("lastUpdateTime", lastUpdateTime)
.commit();
commit(fileSupplier(
"/data/local/tmp/silentupdatetest/CtsSilentUpdateTestCases.apk"),
false /* requireUserAction */, true /* dontKillApp */);
}
@Test
public void selfUpdate_updateApplied() throws Exception {
// verify persisted last update time is no longer current
Assert.assertTrue("lastUpdateTime should be increased if install correctly applied",
getLastUpdateTime() > getContext().getSharedPreferences("default", MODE_PRIVATE)
.getLong("lastUpdateTime", 0));
}
@Test
public void setRequireUserAction_throwsOnIllegalArgument() {
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
params.setRequireUserAction(USER_ACTION_UNSPECIFIED);
params.setRequireUserAction(USER_ACTION_REQUIRED);
params.setRequireUserAction(USER_ACTION_NOT_REQUIRED);
try {
params.setRequireUserAction(-1);
fail("Should not be able to setRequireUserAction to -1");
} catch (IllegalArgumentException e) {
// pass!
}
try {
params.setRequireUserAction(3);
fail("Should not be able to setRequireUserAction to 3");
} catch (IllegalArgumentException e) {
// pass!
}
}
@Test
public void silentInstallRepeatedly_RequiresUserAction() throws Exception {
Assert.assertEquals("The first silent update should succeed",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(CURRENT_APK));
Assert.assertEquals("The repeated silent install invoked within the throttle time "
+ "should require user action",
PackageInstaller.STATUS_PENDING_USER_ACTION,
silentInstallResource(CURRENT_APK));
}
@Test
public void silentInstallRepeatedly_withUnlimitedSilentUpdates_succeed() throws Exception {
setUnlimitedSilentUpdates(INSTALLER_PACKAGE_NAME);
Assert.assertEquals("The first silent update should succeed",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(CURRENT_APK));
Assert.assertEquals("The repeated silent update should succeed",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(CURRENT_APK));
}
@Test
public void silentInstallRepeatedly_waitForThrottleTime_succeed() throws Exception {
setSilentUpdatesThrottleTime(SILENT_UPDATE_THROTTLE_TIME_SECOND);
Assert.assertEquals("The first silent update should succeed",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(CURRENT_APK));
SystemClock.sleep(TimeUnit.SECONDS.toMillis(SILENT_UPDATE_THROTTLE_TIME_SECOND));
Assert.assertEquals("The repeated silent update should succeed",
PackageInstaller.STATUS_SUCCESS,
silentInstallResource(CURRENT_APK));
}
private int silentInstallResource(String resourceName) throws Exception {
return install(resourceSupplier(resourceName), false);
}
private int installResource(String resourceName, Boolean requireUserAction) throws Exception {
return install(resourceSupplier(resourceName), requireUserAction);
}
private int install(Supplier<InputStream> apkStreamSupplier, Boolean requireUserAction)
throws Exception {
InstallStatusListener isl = commit(apkStreamSupplier, requireUserAction,
false /* dontKillApp */);
final Intent statusUpdate = isl.getResult();
final int result =
statusUpdate.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
if (result != PackageInstaller.STATUS_SUCCESS) {
final int sessionId =
statusUpdate.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, Integer.MIN_VALUE);
getContext().getPackageManager().getPackageInstaller().abandonSession(sessionId);
}
return result;
}
private InstallStatusListener commit(Supplier<InputStream> apkStreamSupplier,
Boolean requireUserAction, boolean dontKillApp) throws IOException {
final Context context = getContext();
final PackageInstaller installer = context.getPackageManager().getPackageInstaller();
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
if (requireUserAction != null) {
params.setRequireUserAction(requireUserAction
? USER_ACTION_REQUIRED
: USER_ACTION_NOT_REQUIRED);
}
params.setDontKillApp(dontKillApp);
int sessionId = installer.createSession(params);
Assert.assertEquals("SessionInfo.getRequireUserAction and "
+ "SessionParams.setRequireUserAction are not equal",
installer.getSessionInfo(sessionId).getRequireUserAction(),
requireUserAction == null
? USER_ACTION_UNSPECIFIED
: requireUserAction == Boolean.TRUE
? USER_ACTION_REQUIRED
: SessionParams.USER_ACTION_NOT_REQUIRED);
final PackageInstaller.Session session = installer.openSession(sessionId);
try(OutputStream os = session.openWrite("apk", 0, -1)) {
try (InputStream is = apkStreamSupplier.get()) {
byte[] buffer = new byte[4096];
int n;
while ((n = is.read(buffer)) >= 0) {
os.write(buffer, 0, n);
}
}
}
InstallStatusListener isl = new InstallStatusListener();
session.commit(isl.getIntentSender());
return isl;
}
private Supplier<InputStream> resourceSupplier(String resourceName) {
return () -> {
final InputStream resourceAsStream =
getClass().getClassLoader().getResourceAsStream(resourceName);
if (resourceAsStream == null) {
throw new RuntimeException("Resource " + resourceName + " could not be found.");
}
return resourceAsStream;
};
}
private Supplier<InputStream> fileSupplier(String fileName) {
return () -> {
try {
return new FileInputStream(fileName);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
};
}
private long getLastUpdateTime() throws PackageManager.NameNotFoundException {
final PackageInfo packageInfo = getContext().getPackageManager().getPackageInfo(
getContext().getPackageName(), 0);
final long lastUpdateTime = packageInfo.lastUpdateTime;
return lastUpdateTime;
}
private void setUnlimitedSilentUpdates(String installerPackageName) {
final StringBuilder cmd = new StringBuilder(
"pm set-silent-updates-policy --allow-unlimited-silent-updates ");
cmd.append(installerPackageName);
runShellCommand(cmd.toString());
}
private void setSilentUpdatesThrottleTime(long throttleTimeInSeconds) {
final StringBuilder cmd = new StringBuilder(
"pm set-silent-updates-policy --throttle-time ");
cmd.append(throttleTimeInSeconds);
runShellCommand(cmd.toString());
}
private void resetSilentUpdatesPolicy() {
final StringBuilder cmd = new StringBuilder("pm set-silent-updates-policy --reset");
runShellCommand(cmd.toString());
}
public static class InstallStatusListener extends BroadcastReceiver {
private final BlockingQueue<Intent> mResults = new LinkedBlockingQueue<>();
@Override
public void onReceive(Context context, Intent intent) {
mResults.add(intent);
}
public IntentSender getIntentSender() {
final Context context = getContext();
final String action = UUID.randomUUID().toString();
context.registerReceiver(this, new IntentFilter(action));
Intent intent = new Intent(action);
PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, FLAG_MUTABLE);
return pending.getIntentSender();
}
public Intent getResult() throws InterruptedException {
return mResults.take();
}
}
}