Java tutorial
/* * Copyright 2014-present Facebook, Inc. * * 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.facebook.buck.android; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.CollectingOutputReceiver; import com.android.ddmlib.IDevice; import com.android.ddmlib.InstallException; import com.facebook.buck.android.agent.util.AgentUtil; import com.facebook.buck.event.BuckEventBus; import com.facebook.buck.event.ConsoleEvent; import com.facebook.buck.event.InstallEvent; import com.facebook.buck.event.PerfEventId; import com.facebook.buck.event.SimplePerfEvent; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import com.facebook.buck.rules.ExopackageInfo; import com.facebook.buck.rules.InstallableApk; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.util.NamedTemporaryFile; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.base.Splitter; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * ExopackageInstaller manages the installation of apps with the "exopackage" flag set to true. */ public class ExopackageInstaller { private static final Logger LOG = Logger.get(ExopackageInstaller.class); /** * Prefix of the path to the agent apk on the device. */ private static final String AGENT_DEVICE_PATH = "/data/app/" + AgentUtil.AGENT_PACKAGE_NAME; /** * Command line to invoke the agent on the device. */ private static final String JAVA_AGENT_COMMAND = "dalvikvm -classpath " + AGENT_DEVICE_PATH + "-1.apk:" + AGENT_DEVICE_PATH + "-2.apk:" + AGENT_DEVICE_PATH + "-1/base.apk:" + AGENT_DEVICE_PATH + "-2/base.apk " + "com.facebook.buck.android.agent.AgentMain "; /** * Maximum length of commands that can be passed to "adb shell". */ private static final int MAX_ADB_COMMAND_SIZE = 1019; private static final Path SECONDARY_DEX_DIR = Paths.get("secondary-dex"); private static final Path NATIVE_LIBS_DIR = Paths.get("native-libs"); @VisibleForTesting static final Pattern DEX_FILE_PATTERN = Pattern.compile("secondary-([0-9a-f]+)\\.[\\w.-]*"); @VisibleForTesting static final Pattern NATIVE_LIB_PATTERN = Pattern.compile("native-([0-9a-f]+)\\.so"); private static final Pattern LINE_ENDING = Pattern.compile("\r?\n"); private final ProjectFilesystem projectFilesystem; private final BuckEventBus eventBus; private final AdbHelper adbHelper; private final InstallableApk apkRule; private final String packageName; private final Path dataRoot; private final ExopackageInfo exopackageInfo; /** * The next port number to use for communicating with the agent on a device. * This resets for every instance of ExopackageInstaller, * but is incremented for every device we are installing on when using "-x". */ private final AtomicInteger nextAgentPort = new AtomicInteger(2828); @VisibleForTesting static class PackageInfo { final String apkPath; final String nativeLibPath; final String versionCode; private PackageInfo(String apkPath, String nativeLibPath, String versionCode) { this.nativeLibPath = nativeLibPath; this.apkPath = apkPath; this.versionCode = versionCode; } } public ExopackageInstaller(ExecutionContext context, AdbHelper adbHelper, InstallableApk apkRule) { this.adbHelper = adbHelper; this.projectFilesystem = apkRule.getProjectFilesystem(); this.eventBus = context.getBuckEventBus(); this.apkRule = apkRule; this.packageName = AdbHelper.tryToExtractPackageNameFromManifest(apkRule); this.dataRoot = Paths.get("/data/local/tmp/exopackage/").resolve(packageName); Preconditions.checkArgument(AdbHelper.PACKAGE_NAME_PATTERN.matcher(packageName).matches()); Optional<ExopackageInfo> exopackageInfo = apkRule.getExopackageInfo(); Preconditions.checkArgument(exopackageInfo.isPresent()); this.exopackageInfo = exopackageInfo.get(); } /** * Installs the app specified in the constructor. This object should be discarded afterward. */ public synchronized boolean install(boolean quiet) throws InterruptedException { InstallEvent.Started started = InstallEvent.started(apkRule.getBuildTarget()); eventBus.post(started); boolean success = adbHelper.adbCall(new AdbHelper.AdbCallable() { @Override public boolean call(IDevice device) throws Exception { try { return new SingleDeviceInstaller(device, nextAgentPort.getAndIncrement()).doInstall(); } catch (Exception e) { throw new RuntimeException("Failed to install exopackage on " + device, e); } } @Override public String toString() { return "install exopackage"; } }, quiet); eventBus.post(InstallEvent.finished(started, success, Optional.empty(), Optional.of(AdbHelper.tryToExtractPackageNameFromManifest(apkRule)))); return success; } /** * Helper class to manage the state required to install on a single device. */ private class SingleDeviceInstaller { /** * Device that we are installing onto. */ private final IDevice device; /** * Port to use for sending files to the agent. */ private final int agentPort; /** * True iff we should use the native agent. */ private boolean useNativeAgent = true; /** * Set after the agent is installed. */ @Nullable private String nativeAgentPath; private SingleDeviceInstaller(IDevice device, int agentPort) { this.device = device; this.agentPort = agentPort; } boolean doInstall() throws Exception { Optional<PackageInfo> agentInfo = installAgentIfNecessary(); if (!agentInfo.isPresent()) { return false; } nativeAgentPath = agentInfo.get().nativeLibPath; determineBestAgent(); final File apk = apkRule.getProjectFilesystem().resolve(apkRule.getApkPath()).toFile(); // TODO(dreiss): Support SD installation. final boolean installViaSd = false; if (shouldAppBeInstalled()) { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "install_exo_apk")) { boolean success = adbHelper.installApkOnDevice(device, apk, installViaSd, false); if (!success) { return false; } } } if (exopackageInfo.getDexInfo().isPresent()) { installSecondaryDexFiles(); } if (exopackageInfo.getNativeLibsInfo().isPresent()) { installNativeLibraryFiles(); } // TODO(dreiss): Make this work on Gingerbread. try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "kill_app")) { AdbHelper.executeCommandWithErrorChecking(device, "am force-stop " + packageName); } return true; } private void installSecondaryDexFiles() throws Exception { final ImmutableMap<String, Path> hashToSources = getRequiredDexFiles(); final ImmutableSet<String> requiredHashes = hashToSources.keySet(); final ImmutableSet<String> presentHashes = prepareSecondaryDexDir(requiredHashes); final Set<String> hashesToInstall = Sets.difference(requiredHashes, presentHashes); Map<String, Path> filesToInstallByHash = Maps.filterKeys(hashToSources, hashesToInstall::contains); // This is a bit gross. It was a late addition. Ideally, we could eliminate this, but // it wouldn't be terrible if we don't. We store the dexed jars on the device // with the full SHA-1 hashes in their names. This is the format that the loader uses // internally, so ideally we would just load them in place. However, the code currently // expects to be able to copy the jars from a directory that matches the name in the // metadata file, like "secondary-1.dex.jar". We don't want to give up putting the // hashes in the file names (because we use that to skip re-uploads), so just hack // the metadata file to have hash-like names. String metadataContents = com.google.common.io.Files .toString(projectFilesystem.resolve(exopackageInfo.getDexInfo().get().getMetadata()).toFile(), Charsets.UTF_8) .replaceAll("secondary-(\\d+)\\.dex\\.jar (\\p{XDigit}{40}) ", "secondary-$2.dex.jar $2 "); installFiles("secondary_dex", ImmutableMap.copyOf(filesToInstallByHash), metadataContents, "secondary-%s.dex.jar", SECONDARY_DEX_DIR); } private ImmutableList<String> getDeviceAbis() throws Exception { ImmutableList.Builder<String> abis = ImmutableList.builder(); // Rare special indigenous to Lollipop devices String abiListProperty = getProperty("ro.product.cpu.abilist"); if (!abiListProperty.isEmpty()) { abis.addAll(Splitter.on(',').splitToList(abiListProperty)); } else { String abi1 = getProperty("ro.product.cpu.abi"); if (abi1.isEmpty()) { throw new RuntimeException("adb returned empty result for ro.product.cpu.abi property."); } abis.add(abi1); String abi2 = getProperty("ro.product.cpu.abi2"); if (!abi2.isEmpty()) { abis.add(abi2); } } return abis.build(); } private void installNativeLibraryFiles() throws Exception { ImmutableMultimap<String, Path> allLibraries = getAllLibraries(); ImmutableSet.Builder<String> providedLibraries = ImmutableSet.builder(); for (String abi : getDeviceAbis()) { ImmutableMap<String, Path> libraries = getRequiredLibrariesForAbi(allLibraries, abi, providedLibraries.build()); installNativeLibrariesForAbi(abi, libraries); providedLibraries.addAll(libraries.keySet()); } } private void installNativeLibrariesForAbi(String abi, ImmutableMap<String, Path> libraries) throws Exception { if (libraries.isEmpty()) { return; } String metadataContents = Joiner.on('\n') .join(FluentIterable.from(libraries.entrySet()).transform(input -> { String hash = input.getKey(); String filename = input.getValue().getFileName().toString(); int index = filename.indexOf('.'); String libname = index == -1 ? filename : filename.substring(0, index); return String.format("%s native-%s.so", libname, hash); })); ImmutableSet<String> requiredHashes = libraries.keySet(); ImmutableSet<String> presentHashes = prepareNativeLibsDir(abi, requiredHashes); Map<String, Path> filesToInstallByHash = Maps.filterKeys(libraries, Predicates.not(presentHashes::contains)); installFiles("native_library", ImmutableMap.copyOf(filesToInstallByHash), metadataContents, "native-%s.so", NATIVE_LIBS_DIR.resolve(abi)); } /** * Sets {@link #useNativeAgent} to true on pre-L devices, because our native agent is built * without -fPIC. The java agent works fine on L as long as we don't use it for mkdir. */ private void determineBestAgent() throws Exception { String value = getProperty("ro.build.version.sdk"); try { if (Integer.valueOf(value.trim()) > 19) { useNativeAgent = false; } } catch (NumberFormatException exn) { useNativeAgent = false; } } private String getAgentCommand() { if (useNativeAgent) { return nativeAgentPath + "/libagent.so "; } else { return JAVA_AGENT_COMMAND; } } private Optional<PackageInfo> getPackageInfo(final String packageName) throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, PerfEventId.of("get_package_info"), "package", packageName)) { /* "dumpsys package <package>" produces output that looks like Package [com.facebook.katana] (4229ce68): userId=10145 gids=[1028, 1015, 3003] pkg=Package{42690b80 com.facebook.katana} codePath=/data/app/com.facebook.katana-1.apk resourcePath=/data/app/com.facebook.katana-1.apk nativeLibraryPath=/data/app-lib/com.facebook.katana-1 versionCode=1640376 targetSdk=14 versionName=8.0.0.0.23 ... */ // We call "pm path" because "dumpsys package" returns valid output if an app has been // uninstalled using the "--keepdata" option. "pm path", on the other hand, returns an empty // output in that case. String lines = AdbHelper.executeCommandWithErrorChecking(device, String.format("pm path %s; dumpsys package %s", packageName, packageName)); return parsePathAndPackageInfo(packageName, lines); } } /** * @return PackageInfo for the agent, or absent if installation failed. */ private Optional<PackageInfo> installAgentIfNecessary() throws Exception { Optional<PackageInfo> agentInfo = getPackageInfo(AgentUtil.AGENT_PACKAGE_NAME); if (!agentInfo.isPresent()) { LOG.debug("Agent not installed. Installing."); return installAgentApk(); } LOG.debug("Agent version: %s", agentInfo.get().versionCode); if (!agentInfo.get().versionCode.equals(AgentUtil.AGENT_VERSION_CODE)) { // Always uninstall before installing. We might be downgrading, which requires // an uninstall, or we might just want a clean installation. uninstallAgent(); return installAgentApk(); } return agentInfo; } private void uninstallAgent() throws InstallException { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "uninstall_old_agent")) { device.uninstallPackage(AgentUtil.AGENT_PACKAGE_NAME); } } private Optional<PackageInfo> installAgentApk() throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "install_agent_apk")) { String apkFileName = System.getProperty("buck.android_agent_path"); if (apkFileName == null) { throw new RuntimeException("Android agent apk path not specified in properties"); } File apkPath = new File(apkFileName); boolean success = adbHelper.installApkOnDevice(device, apkPath, /* installViaSd */ false, /* quiet */ false); if (!success) { return Optional.empty(); } return getPackageInfo(AgentUtil.AGENT_PACKAGE_NAME); } } private boolean shouldAppBeInstalled() throws Exception { Optional<PackageInfo> appPackageInfo = getPackageInfo(packageName); if (!appPackageInfo.isPresent()) { eventBus.post(ConsoleEvent.info("App not installed. Installing now.")); return true; } LOG.debug("App path: %s", appPackageInfo.get().apkPath); String installedAppSignature = getInstalledAppSignature(appPackageInfo.get().apkPath); String localAppSignature = AgentUtil .getJarSignature(apkRule.getProjectFilesystem().resolve(apkRule.getApkPath()).toString()); LOG.debug("Local app signature: %s", localAppSignature); LOG.debug("Remote app signature: %s", installedAppSignature); if (!installedAppSignature.equals(localAppSignature)) { LOG.debug("App signatures do not match. Must re-install."); return true; } LOG.debug("App signatures match. No need to install."); return false; } private String getInstalledAppSignature(final String packagePath) throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "get_app_signature")) { String command = getAgentCommand() + "get-signature " + packagePath; LOG.debug("Executing %s", command); String output = AdbHelper.executeCommandWithErrorChecking(device, command); String result = output.trim(); if (result.contains("\n") || result.contains("\r")) { throw new IllegalStateException("Unexpected return from get-signature:\n" + output); } return result; } } private ImmutableMap<String, Path> getRequiredDexFiles() throws IOException { ExopackageInfo.DexInfo dexInfo = exopackageInfo.getDexInfo().get(); ImmutableMultimap<String, Path> multimap = parseExopackageInfoMetadata(dexInfo.getMetadata(), dexInfo.getDirectory(), projectFilesystem); // Convert multimap to a map, because every key should have only one value. ImmutableMap.Builder<String, Path> builder = ImmutableMap.builder(); for (Map.Entry<String, Path> entry : multimap.entries()) { builder.put(entry); } return builder.build(); } private ImmutableSet<String> prepareSecondaryDexDir(ImmutableSet<String> requiredHashes) throws Exception { return prepareDirectory("secondary-dex", DEX_FILE_PATTERN, requiredHashes); } private ImmutableSet<String> prepareNativeLibsDir(String abi, ImmutableSet<String> requiredHashes) throws Exception { return prepareDirectory("native-libs/" + abi, NATIVE_LIB_PATTERN, requiredHashes); } private ImmutableSet<String> prepareDirectory(String dirname, Pattern filePattern, ImmutableSet<String> requiredHashes) throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "prepare_" + dirname)) { String dirPath = dataRoot.resolve(dirname).toString(); mkDirP(dirPath); String output = AdbHelper.executeCommandWithErrorChecking(device, "ls " + dirPath); ImmutableSet.Builder<String> foundHashes = ImmutableSet.builder(); ImmutableSet.Builder<String> filesToDelete = ImmutableSet.builder(); processLsOutput(output, filePattern, requiredHashes, foundHashes, filesToDelete); String commandPrefix = "cd " + dirPath + " && rm "; // Add a fudge factor for separators and error checking. final int overhead = commandPrefix.length() + 100; for (List<String> rmArgs : chunkArgs(filesToDelete.build(), MAX_ADB_COMMAND_SIZE - overhead)) { String command = commandPrefix + Joiner.on(' ').join(rmArgs); LOG.debug("Executing %s", command); AdbHelper.executeCommandWithErrorChecking(device, command); } return foundHashes.build(); } } private void installFiles(String filesType, ImmutableMap<String, Path> filesToInstallByHash, String metadataFileContents, String filenameFormat, Path destinationDirRelativeToDataRoot) throws Exception { try (SimplePerfEvent.Scope ignored1 = SimplePerfEvent.scope(eventBus, "multi_install_" + filesType)) { device.createForward(agentPort, agentPort); try { for (Map.Entry<String, Path> entry : filesToInstallByHash.entrySet()) { Path destination = destinationDirRelativeToDataRoot .resolve(String.format(filenameFormat, entry.getKey())); Path source = entry.getValue(); try (SimplePerfEvent.Scope ignored2 = SimplePerfEvent.scope(eventBus, "install_" + filesType)) { installFile(device, agentPort, destination, source); } } try (SimplePerfEvent.Scope ignored3 = SimplePerfEvent.scope(eventBus, "install_" + filesType + "_metadata")) { try (NamedTemporaryFile temp = new NamedTemporaryFile("metadata", "tmp")) { com.google.common.io.Files.write(metadataFileContents.getBytes(Charsets.UTF_8), temp.get().toFile()); installFile(device, agentPort, destinationDirRelativeToDataRoot.resolve("metadata.txt"), temp.get()); } } } finally { try { device.removeForward(agentPort, agentPort); } catch (AdbCommandRejectedException e) { LOG.warn(e, "Failed to remove adb forward on port %d for device %s", agentPort, device); eventBus.post(ConsoleEvent .warning("Failed to remove adb forward %d. This is not necessarily a problem\n" + "because it will be recreated during the next exopackage installation.\n" + "See the log for the full exception.", agentPort)); } } } } private void installFile(IDevice device, final int port, Path pathRelativeToDataRoot, final Path relativeSource) throws Exception { final Path source = projectFilesystem.resolve(relativeSource); CollectingOutputReceiver receiver = new CollectingOutputReceiver() { private boolean sentPayload = false; @Override public void addOutput(byte[] data, int offset, int length) { super.addOutput(data, offset, length); if (!sentPayload && getOutput().length() >= AgentUtil.TEXT_SECRET_KEY_SIZE) { LOG.verbose("Got key: %s", getOutput().trim()); sentPayload = true; try (Socket clientSocket = new Socket("localhost", port)) { LOG.verbose("Connected"); OutputStream outToDevice = clientSocket.getOutputStream(); outToDevice.write(getOutput().substring(0, AgentUtil.TEXT_SECRET_KEY_SIZE).getBytes()); LOG.verbose("Wrote key"); com.google.common.io.Files.asByteSource(source.toFile()).copyTo(outToDevice); LOG.verbose("Wrote file"); } catch (IOException e) { throw new RuntimeException(e); } } } }; String targetFileName = projectFilesystem.resolve(dataRoot.resolve(pathRelativeToDataRoot)).toString(); String command = "umask 022 && " + getAgentCommand() + "receive-file " + port + " " + Files.size(source) + " " + targetFileName + " ; echo -n :$?"; LOG.debug("Executing %s", command); // If we fail to execute the command, stash the exception. My experience during development // has been that the exception from checkReceiverOutput is more actionable. Exception shellException = null; try { device.executeShellCommand(command, receiver); } catch (Exception e) { shellException = e; } try { AdbHelper.checkReceiverOutput(command, receiver); } catch (Exception e) { if (shellException != null) { e.addSuppressed(shellException); } throw e; } if (shellException != null) { throw shellException; } // The standard Java libraries on Android always create new files un-readable by other users. // We use the shell user or root to create these files, so we need to explicitly set the mode // to allow the app to read them. Ideally, the agent would do this automatically, but // there's no easy way to do this in Java. We can drop this if we drop support for the // Java agent. AdbHelper.executeCommandWithErrorChecking(device, "chmod 644 " + targetFileName); } private String getProperty(String property) throws Exception { return AdbHelper.executeCommandWithErrorChecking(device, "getprop " + property).trim(); } private void mkDirP(String dirpath) throws Exception { // Kind of a hack here. The java agent can't force the proper permissions on the // directories it creates, so we use the command-line "mkdir -p" instead of the java agent. // Fortunately, "mkdir -p" seems to work on all devices where we use use the java agent. String mkdirP = useNativeAgent ? getAgentCommand() + "mkdir-p" : "mkdir -p"; AdbHelper.executeCommandWithErrorChecking(device, "umask 022 && " + mkdirP + " " + dirpath); } } private ImmutableMultimap<String, Path> getAllLibraries() throws IOException { ExopackageInfo.NativeLibsInfo nativeLibsInfo = exopackageInfo.getNativeLibsInfo().get(); return parseExopackageInfoMetadata(nativeLibsInfo.getMetadata(), nativeLibsInfo.getDirectory(), projectFilesystem); } private ImmutableMap<String, Path> getRequiredLibrariesForAbi(ImmutableMultimap<String, Path> allLibraries, String abi, ImmutableSet<String> ignoreLibraries) { return filterLibrariesForAbi(exopackageInfo.getNativeLibsInfo().get().getDirectory(), allLibraries, abi, ignoreLibraries); } @VisibleForTesting static ImmutableMap<String, Path> filterLibrariesForAbi(Path nativeLibsDir, ImmutableMultimap<String, Path> allLibraries, String abi, ImmutableSet<String> ignoreLibraries) { ImmutableMap.Builder<String, Path> filteredLibraries = ImmutableMap.builder(); for (Map.Entry<String, Path> entry : allLibraries.entries()) { Path relativePath = nativeLibsDir.relativize(entry.getValue()); // relativePath is of the form libs/x86/foo.so, or assetLibs/x86/foo.so etc. Preconditions.checkState(relativePath.getNameCount() == 3); Preconditions.checkState(relativePath.getName(0).toString().equals("libs") || relativePath.getName(0).toString().equals("assetLibs")); String libAbi = relativePath.getParent().getFileName().toString(); String libName = relativePath.getFileName().toString(); if (libAbi.equals(abi) && !ignoreLibraries.contains(libName)) { filteredLibraries.put(entry); } } return filteredLibraries.build(); } /** * Parses a text file which is supposed to be in the following format: * "file_path_without_spaces file_hash ...." i.e. it parses the first two columns of each line * and ignores the rest of it. * * @return A multi map from the file hash to its path, which equals the raw path resolved against * {@code resolvePathAgainst}. */ @VisibleForTesting static ImmutableMultimap<String, Path> parseExopackageInfoMetadata(Path metadataTxt, Path resolvePathAgainst, ProjectFilesystem filesystem) throws IOException { ImmutableMultimap.Builder<String, Path> builder = ImmutableMultimap.builder(); for (String line : filesystem.readLines(metadataTxt)) { // ignore lines that start with '.' if (line.startsWith(".")) { continue; } List<String> parts = Splitter.on(' ').splitToList(line); if (parts.size() < 2) { throw new RuntimeException("Illegal line in metadata file: " + line); } builder.put(parts.get(1), resolvePathAgainst.resolve(parts.get(0))); } return builder.build(); } @VisibleForTesting static Optional<PackageInfo> parsePathAndPackageInfo(String packageName, String rawOutput) { Iterable<String> lines = Splitter.on(LINE_ENDING).omitEmptyStrings().split(rawOutput); String pmPathPrefix = "package:"; String pmPath = null; for (String line : lines) { // Ignore silly linker warnings about non-PIC code on emulators if (!line.startsWith("WARNING: linker: ")) { pmPath = line; break; } } if (pmPath == null || !pmPath.startsWith(pmPathPrefix)) { LOG.warn("unable to locate package path for [" + packageName + "]"); return Optional.empty(); } final String packagePrefix = " Package [" + packageName + "] ("; final String otherPrefix = " Package ["; boolean sawPackageLine = false; final Splitter splitter = Splitter.on('=').limit(2); String codePath = null; String resourcePath = null; String nativeLibPath = null; String versionCode = null; for (String line : lines) { // Just ignore everything until we see the line that says we are in the right package. if (line.startsWith(packagePrefix)) { sawPackageLine = true; continue; } // This should never happen, but if we do see a different package, stop parsing. if (line.startsWith(otherPrefix)) { break; } // Ignore lines before our package. if (!sawPackageLine) { continue; } // Parse key-value pairs. List<String> parts = splitter.splitToList(line.trim()); if (parts.size() != 2) { continue; } switch (parts.get(0)) { case "codePath": codePath = parts.get(1); break; case "resourcePath": resourcePath = parts.get(1); break; case "nativeLibraryPath": nativeLibPath = parts.get(1); break; // Lollipop uses this name. Not sure what's "legacy" about it yet. // Maybe something to do with 64-bit? // Might need to update if people report failures. case "legacyNativeLibraryDir": nativeLibPath = parts.get(1); break; case "versionCode": // Extra split to get rid of the SDK thing. versionCode = parts.get(1).split(" ", 2)[0]; break; default: break; } } if (!sawPackageLine) { return Optional.empty(); } Preconditions.checkNotNull(codePath, "Could not find codePath"); Preconditions.checkNotNull(resourcePath, "Could not find resourcePath"); Preconditions.checkNotNull(nativeLibPath, "Could not find nativeLibraryPath"); Preconditions.checkNotNull(versionCode, "Could not find versionCode"); if (!codePath.equals(resourcePath)) { throw new IllegalStateException("Code and resource path do not match"); } // Lollipop doesn't give the full path to the apk anymore. Not sure why it's "base.apk". if (!codePath.endsWith(".apk")) { codePath += "/base.apk"; } return Optional.of(new PackageInfo(codePath, nativeLibPath, versionCode)); } /** * @param output Output of "ls" command. * @param filePattern A {@link Pattern} that is used to check if a file is valid, and if it * matches, {@code filePattern.group(1)} should return the hash in the file name. * @param requiredHashes Hashes of dex files required for this apk. * @param foundHashes Builder to receive hashes that we need and were found. * @param toDelete Builder to receive files that we need to delete. */ @VisibleForTesting static void processLsOutput(String output, Pattern filePattern, ImmutableSet<String> requiredHashes, ImmutableSet.Builder<String> foundHashes, ImmutableSet.Builder<String> toDelete) { for (String line : Splitter.on(LINE_ENDING).omitEmptyStrings().split(output)) { if (line.equals("lock")) { continue; } Matcher m = filePattern.matcher(line); if (m.matches()) { if (requiredHashes.contains(m.group(1))) { foundHashes.add(m.group(1)); } else { toDelete.add(line); } } else { toDelete.add(line); } } } /** * Breaks a list of strings into groups whose total size is within some limit. * Kind of like the xargs command that groups arguments to avoid maximum argument length limits. * Except that the limit in adb is about 1k instead of 512k or 2M on Linux. */ @VisibleForTesting static ImmutableList<ImmutableList<String>> chunkArgs(Iterable<String> args, int sizeLimit) { ImmutableList.Builder<ImmutableList<String>> topLevelBuilder = ImmutableList.builder(); ImmutableList.Builder<String> chunkBuilder = ImmutableList.builder(); int chunkSize = 0; for (String arg : args) { if (chunkSize + arg.length() > sizeLimit) { topLevelBuilder.add(chunkBuilder.build()); chunkBuilder = ImmutableList.builder(); chunkSize = 0; } // We don't check for an individual arg greater than the limit. // We just put it in its own chunk and hope for the best. chunkBuilder.add(arg); chunkSize += arg.length(); } ImmutableList<String> tail = chunkBuilder.build(); if (!tail.isEmpty()) { topLevelBuilder.add(tail); } return topLevelBuilder.build(); } }