blob: 02d6c01c0458f1cf6c5f6348c4b65952d6a1c579 [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.tools.deploy.proto.Deploy;
import com.android.tools.deployer.model.DexClass;
import com.android.tools.idea.protobuf.ByteString;
import com.android.utils.ILogger;
import com.google.common.collect.Iterables;
import java.util.List;
import java.util.Map;
/** An object that can perform swaps via an installer or custom redefiners. */
public class ApkSwapper {
private final Installer installer;
private final boolean restart;
private final Map<Integer, ClassRedefiner> debuggerRedefiners;
private final AdbClient adb;
private final ILogger logger;
/**
* @param installer used to perform swaps on device.
* @param restart whether to restart the application or not.
* @param debuggerRedefiners an additional set of redefiners that will handle the swap for the
* given process ids that have debugger attached
*/
public ApkSwapper(
Installer installer,
Map<Integer, ClassRedefiner> debuggerRedefiners,
boolean restart,
AdbClient adb,
ILogger logger) {
this.installer = installer;
this.debuggerRedefiners = debuggerRedefiners;
this.restart = restart;
this.adb = adb;
this.logger = logger;
}
// If a session was created and Android Studio (not the device) still owns it, we must
// abort it on device in order to avoid a session leak.
Void error(
ApplicationDumper.Dump unused, String sessionId, DexComparator.ChangedClasses unused2) {
// Was a session created?
if (sessionId == null) {
return null;
}
// At this point, a session was created but may have not been handed to the device.
// The session needs to be aborted.
if (ApkPreInstaller.SKIPPED_INSTALLATION.equals(sessionId)) {
return null;
}
// TODO: Getting a String and parsing it here is wrong. The parsing should happen
// in a dedicated object. To lower the volume of code in this CL I did not refactor
// but I will do it in my next CL.
String abortResult = adb.abortSession(sessionId);
if (!abortResult.startsWith("Success")) {
logger.warning("Unable to abandon session: '%s'", abortResult);
}
return null;
}
/**
* Performs the swap.
*
* @param dump the application dump
* @param sessionId the installation session
* @param toSwap the actual dex classes to swap.
*/
public boolean swap(
ApplicationDumper.Dump dump,
String sessionId,
DexComparator.ChangedClasses changedClasses)
throws DeployerException {
// The application dump contains a map of [package name --> process ids]. If there are no
// packages with running processes, the swap cannot be performed.
if (dump.packagePids.isEmpty()) {
throw DeployerException.unknownProcess();
}
// The native installer can't handle swapping more than one package at a time. The dump does
// not enforce this limitation, so we need to check here to make sure we haven't been given
// multiple applications to swap. This could happen if an instrumentation package targets
// multiple other packages; we may elect to fix this limitation in the future.
if (dump.packagePids.size() > 1) {
throw DeployerException.swapMultiplePackages();
}
// TODO: Add a new installer command? Add a new flag?
Deploy.SwapRequest swapRequest = buildSwapRequest(dump, sessionId, changedClasses);
boolean needAgents = isSwapRequestInstallOnly(swapRequest);
Thread t = null;
// A hack to get around lambda capture having to be effectively final. This single item array is captured by the lambda but we
// can still set its content should and exception occurs.
DeployerException[] exceptions = new DeployerException[1];
if (needAgents) {
t =
new Thread(
() -> {
try {
sendSwapRequest(
swapRequest,
new InstallerBasedClassRedefiner(installer));
} catch (DeployerException e) {
exceptions[0] = e;
}
});
t.start();
}
// Do the debugger swap.
for (Map.Entry<Integer, ClassRedefiner> entry : debuggerRedefiners.entrySet()) {
sendSwapRequest(swapRequest, entry.getValue());
}
// Wait for installer to come back.
if (t != null) {
try {
t.join();
} catch (InterruptedException e) {
throw DeployerException.interrupted(e.getMessage());
}
if (exceptions[0] != null) {
throw exceptions[0];
}
} else {
// We didn't start the installer request before since it will always succeed. Now that debugger swap is done,
// we can commit the install.
sendSwapRequest(swapRequest, new InstallerBasedClassRedefiner(installer));
}
return true;
}
private Deploy.SwapRequest buildSwapRequest(
ApplicationDumper.Dump dump,
String sessionId,
DexComparator.ChangedClasses changedClasses)
throws DeployerException {
Map.Entry<String, List<Integer>> onlyPackage =
Iterables.getOnlyElement(dump.packagePids.entrySet());
return buildSwapRequest(
onlyPackage.getKey(), onlyPackage.getValue(), dump.arch, sessionId, changedClasses);
}
private Deploy.SwapRequest buildSwapRequest(
String packageId,
List<Integer> pids,
Deploy.Arch arch,
String sessionId,
DexComparator.ChangedClasses changedClasses)
throws DeployerException {
Deploy.SwapRequest.Builder request =
Deploy.SwapRequest.newBuilder()
.setPackageName(packageId)
.setRestartActivity(restart)
.setSessionId(sessionId);
for (DexClass clazz : changedClasses.newClasses) {
request.addNewClasses(
Deploy.ClassDef.newBuilder()
.setName(clazz.name)
.setDex(ByteString.copyFrom(clazz.code)));
}
for (DexClass clazz : changedClasses.modifiedClasses) {
request.addModifiedClasses(
Deploy.ClassDef.newBuilder()
.setName(clazz.name)
.setDex(ByteString.copyFrom(clazz.code)));
}
int extraAgents = 0;
for (Integer pid : pids) {
if (debuggerRedefiners.containsKey(pid)) {
ClassRedefiner redefiner = debuggerRedefiners.get(pid);
switch (redefiner.canRedefineClass().support) {
case FULL:
continue;
case NEEDS_AGENT_SERVER:
extraAgents++;
continue;
case MAIN_THREAD_RUNNING:
request.addProcessIds(pid);
continue;
case NONE:
throw DeployerException.operationNotSupported(
"The redefiner is not able to swap the current state of the debug application. "
+ "All available threads are suspended but not on a breakpoint.");
}
} else {
request.addProcessIds(pid);
}
}
request.setExtraAgents(extraAgents);
request.setArch(arch);
return request.build();
}
/**
* Check if the swap request expects zero agents to talk to the agent server.
*
* <p>Such swap request will always succeed and, therefore, always install the APK.
*/
private boolean isSwapRequestInstallOnly(Deploy.SwapRequest request) {
return request.getProcessIdsCount() > 0 || request.getExtraAgents() > 0;
}
private static void sendSwapRequest(Deploy.SwapRequest request, ClassRedefiner redefiner)
throws DeployerException {
Deploy.SwapResponse swapResponse = redefiner.redefine(request);
new InstallerResponseHandler(
InstallerResponseHandler.RedefinitionCapability.MOFIFY_CODE_ONLY)
.handle(swapResponse);
}
}