blob: 02d31cfbca79830aa33d9cb42198fbe3470211ae [file] [log] [blame]
/*
* Copyright (C) 2018 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.tools.deployer;
import com.android.annotations.Trace;
import com.android.tools.deploy.proto.Deploy;
import com.android.tools.deployer.model.Apk;
import com.android.tools.deployer.model.FileDiff;
import com.android.utils.ILogger;
import com.android.utils.Pair;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ApkPreInstaller {
/**
* Thrown for cases where we bail out of using delta install. This is distinct from the case
* where the APK is unchanged and we don't need to install anything.
*/
public static class DeltaPreInstallException extends Exception {
public DeltaPreInstallException(String msg) {
super(msg);
}
public DeltaPreInstallException(Exception e) {
super(e);
}
}
public static final String SKIPPED_INSTALLATION = "<SKIPPED-INSTALLATION>";
private final AdbClient adb;
private final Installer installer;
private final ILogger logger;
public ApkPreInstaller(AdbClient adb, Installer installer, ILogger logger) {
this.adb = adb;
this.installer = installer;
this.logger = logger;
}
/**
* Push local .apks files to a device.
*
* <p>First try to perform a fast "delta push" during which only the new parts of the apks are
* transferred to the device. Fallback to standard "install-multiple" full pull if delta push
* fails.
*
* @param remoteContent All APK entries on the device
* @param localContent All APK entries on the local host
* @return Session ID of the install session.
*/
@Trace
public String preinstall(
ApplicationDumper.Dump remoteContent, List<Apk> localContent, List<FileDiff> diffs)
throws DeployerException {
// Build the list of local apks.
HashMap<String, Apk> localApks = new HashMap<>();
for (Apk file : localContent) {
localApks.put(file.name, file);
}
// Build the list of remote apks.
HashMap<String, Apk> remoteApks = new HashMap<>();
for (Apk file : remoteContent.apks) {
remoteApks.put(file.name, file);
}
// Make sure all apks have the same package name and extract it.
String packageName = null;
for (Apk apk : localApks.values()) {
if (packageName == null) {
packageName = apk.packageName;
}
if (!packageName.equals(apk.packageName)) {
throw DeployerException.appIdChanged(packageName, apk.packageName);
}
}
// Attempt a DeltaPreinstall first and fallback on a FullPreinstall if it fails.
try {
return deltaPreinstall(localApks, remoteApks, packageName, diffs);
} catch (DeltaPreInstallException e) {
return fullPreinstall(localApks, packageName);
}
}
@Trace
/** @return Session ID. Empty if all APKs are unchanged from the device. */
private String deltaPreinstall(
HashMap<String, Apk> localApks,
HashMap<String, Apk> remoteApks,
String packageName,
List<FileDiff> diffs)
throws DeltaPreInstallException {
try {
// Pair remote and local apks. Attempt to build an app delta.
List<Pair<Apk, Apk>> pairs = new ArrayList<>();
for (Map.Entry<String, Apk> localApk : localApks.entrySet()) {
if (!remoteApks.keySet().contains(localApk.getValue().name)) {
throw new DeltaPreInstallException("APK names changed.");
}
pairs.add(Pair.of(localApk.getValue(), remoteApks.get(localApk.getValue().name)));
}
Deploy.InstallInfo.Builder pushRequestBuilder = Deploy.InstallInfo.newBuilder();
PatchSet patchSet =
new PatchSetGenerator(
PatchSetGenerator.WhenNoChanges.GENERATE_EMPTY_PATCH, logger)
.generateFromPairs(pairs);
switch (patchSet.getStatus()) {
case NoChanges:
return SKIPPED_INSTALLATION;
case Invalid:
throw new DeltaPreInstallException("Unable to generate patch");
case SizeThresholdExceeded:
throw new DeltaPreInstallException("Patches too big.");
case Ok:
break;
default:
throw new IllegalStateException("Unhandled PatchSet status");
}
List<Deploy.PatchInstruction> patches = patchSet.getPatches();
for (Deploy.PatchInstruction patch : patches) {
logger.info("Patch size %d", patch.getSerializedSize());
}
pushRequestBuilder.addAllPatchInstructions(patches);
boolean inherit =
ApkInstaller.canInherit(localApks.size(), diffs, Deployer.InstallMode.DELTA);
pushRequestBuilder.setInherit(inherit);
pushRequestBuilder.setPackageName(packageName);
// Send the deltaPreinstall request here.
Deploy.InstallInfo info = pushRequestBuilder.build();
Deploy.DeltaPreinstallResponse response = installer.deltaPreinstall(info);
if (response.getStatus().equals(Deploy.DeltaStatus.OK)) {
return response.getSessionId();
} else {
throw new DeltaPreInstallException("Failed to delta pre-install on device.");
}
} catch (IOException e) {
logger.error(e, "Unable to deltaInstall");
throw new DeltaPreInstallException(e);
}
}
@Trace
private String fullPreinstall(HashMap<String, Apk> fullApks, String packageName)
throws DeployerException {
long totalSize = 0;
try {
for (Apk apk : fullApks.values()) {
totalSize += Files.size(Paths.get(apk.path));
}
} catch (IOException e) {
throw DeployerException.preinstallFailed(e.getMessage());
}
String sessionId;
try {
List<String> installOptions =
Arrays.asList(
"package",
"install-create",
"-t",
"-r",
"--dont-kill",
"-S",
Long.toString(totalSize));
String skipVerificationString = adb.getSkipVerificationOption(packageName);
if (skipVerificationString != null) {
installOptions.add(skipVerificationString);
}
byte[] rawResponse =
adb.binder(installOptions.toArray(new String[installOptions.size()]), null);
// Parse result which should be in the form:
// "Success: created install session [X]" where X is the session id.
String stringResponse = new String(rawResponse, StandardCharsets.UTF_8);
if (!stringResponse.startsWith("Success: created install session [")) {
throw DeployerException.preinstallFailed(
"Unable to create session : " + stringResponse);
}
sessionId =
stringResponse.substring(
stringResponse.indexOf('[') + 1, stringResponse.indexOf(']'));
} catch (IOException e) {
throw DeployerException.preinstallFailed(e.getMessage());
}
for (Apk apk : fullApks.values()) {
try {
FileInputStream stream = new FileInputStream(new File(apk.path));
long size = Files.size(Paths.get(apk.path));
byte[] rawResponse =
adb.binder(
new String[] {
"package",
"install-write",
"-S",
Long.toString(size),
sessionId,
apk.name
},
stream);
String stringResponse = new String(rawResponse, "UTF-8");
if (!stringResponse.startsWith("Success")) {
adb.abortSession(sessionId);
throw DeployerException.preinstallFailed(stringResponse);
}
} catch (IOException e) {
adb.abortSession(sessionId);
throw DeployerException.preinstallFailed(e.getMessage());
}
}
return sessionId;
}
}