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.exopackage; import com.facebook.buck.android.AdbHelper; import com.facebook.buck.android.ApkInfo; import com.facebook.buck.android.agent.util.AgentUtil; import com.facebook.buck.core.build.execution.context.ExecutionContext; import com.facebook.buck.core.exceptions.HumanReadableException; import com.facebook.buck.core.sourcepath.resolver.SourcePathResolver; import com.facebook.buck.core.util.log.Logger; import com.facebook.buck.event.BuckEventBus; import com.facebook.buck.event.ConsoleEvent; import com.facebook.buck.event.PerfEventId; import com.facebook.buck.event.SimplePerfEvent; import com.facebook.buck.io.filesystem.ProjectFilesystem; import com.facebook.buck.util.NamedTemporaryFile; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import com.google.common.io.Closer; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; 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); public static final Path EXOPACKAGE_INSTALL_ROOT = Paths.get("/data/local/tmp/exopackage/"); public static final String SECONDARY_DEX_TYPE = "secondary_dex"; public static final String NATIVE_LIBRARY_TYPE = "native_library"; public static final String RESOURCES_TYPE = "resources"; private final ProjectFilesystem projectFilesystem; private final BuckEventBus eventBus; private final SourcePathResolver pathResolver; private final AndroidDevice device; private final String packageName; private final Path dataRoot; public ExopackageInstaller(SourcePathResolver pathResolver, ExecutionContext context, ProjectFilesystem projectFilesystem, String packageName, AndroidDevice device) { this.pathResolver = pathResolver; this.projectFilesystem = projectFilesystem; this.eventBus = context.getBuckEventBus(); this.device = device; this.packageName = packageName; this.dataRoot = EXOPACKAGE_INSTALL_ROOT.resolve(packageName); Preconditions.checkArgument(AdbHelper.PACKAGE_NAME_PATTERN.matcher(packageName).matches()); } /** @return Returns true. */ // TODO(cjhopman): This return value is silly. Change it to be void. public boolean doInstall(ApkInfo apkInfo, @Nullable String processName) throws Exception { if (exopackageEnabled(apkInfo)) { device.mkDirP(dataRoot.toString()); ImmutableSortedSet<Path> presentFiles = device.listDirRecursive(dataRoot); ExopackageInfo exoInfo = apkInfo.getExopackageInfo().get(); installMissingExopackageFiles(presentFiles, exoInfo); finishExoFileInstallation(presentFiles, exoInfo); } installApkIfNecessary(apkInfo); killApp(apkInfo, processName); return true; } public void killApp(ApkInfo apkInfo, @Nullable String processName) throws Exception { // TODO(dreiss): Make this work on Gingerbread. try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "kill_app")) { // If a specific process name is given and we're not installing a full APK, // just kill that process, otherwise kill everything in the package if (shouldAppBeInstalled(apkInfo) || processName == null) { device.stopPackage(packageName); } else { device.killProcess(processName); } } } public void installApkIfNecessary(ApkInfo apkInfo) throws Exception { File apk = pathResolver.getAbsolutePath(apkInfo.getApkPath()).toFile(); // TODO(dreiss): Support SD installation. boolean installViaSd = false; if (shouldAppBeInstalled(apkInfo)) { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "install_exo_apk")) { boolean success = device.installApkOnDevice(apk, installViaSd, false); if (!success) { throw new RuntimeException("Installing Apk failed."); } } } } public void finishExoFileInstallation(ImmutableSortedSet<Path> presentFiles, ExopackageInfo exoInfo) throws Exception { ImmutableSet.Builder<Path> wantedPaths = ImmutableSet.builder(); ImmutableMap.Builder<Path, String> metadata = ImmutableMap.builder(); if (exoInfo.getDexInfo().isPresent()) { DexExoHelper dexExoHelper = new DexExoHelper(pathResolver, projectFilesystem, exoInfo.getDexInfo().get()); wantedPaths.addAll(dexExoHelper.getFilesToInstall().keySet()); metadata.putAll(dexExoHelper.getMetadataToInstall()); } if (exoInfo.getNativeLibsInfo().isPresent()) { NativeExoHelper nativeExoHelper = new NativeExoHelper(() -> { try { return device.getDeviceAbis(); } catch (Exception e) { throw new HumanReadableException("Unable to communicate with device", e); } }, pathResolver, projectFilesystem, exoInfo.getNativeLibsInfo().get()); wantedPaths.addAll(nativeExoHelper.getFilesToInstall().keySet()); metadata.putAll(nativeExoHelper.getMetadataToInstall()); } if (exoInfo.getResourcesInfo().isPresent()) { ResourcesExoHelper resourcesExoHelper = new ResourcesExoHelper(pathResolver, projectFilesystem, exoInfo.getResourcesInfo().get()); wantedPaths.addAll(resourcesExoHelper.getFilesToInstall().keySet()); metadata.putAll(resourcesExoHelper.getMetadataToInstall()); } if (exoInfo.getModuleInfo().isPresent()) { ModuleExoHelper moduleExoHelper = new ModuleExoHelper(pathResolver, projectFilesystem, exoInfo.getModuleInfo().get()); wantedPaths.addAll(moduleExoHelper.getFilesToInstall().keySet()); metadata.putAll(moduleExoHelper.getMetadataToInstall()); } deleteUnwantedFiles(presentFiles, wantedPaths.build()); installMetadata(metadata.build()); } public void installMissingExopackageFiles(ImmutableSortedSet<Path> presentFiles, ExopackageInfo exoInfo) throws Exception { if (exoInfo.getDexInfo().isPresent()) { DexExoHelper dexExoHelper = new DexExoHelper(pathResolver, projectFilesystem, exoInfo.getDexInfo().get()); installMissingFiles(presentFiles, dexExoHelper.getFilesToInstall(), SECONDARY_DEX_TYPE); } if (exoInfo.getNativeLibsInfo().isPresent()) { NativeExoHelper nativeExoHelper = new NativeExoHelper(() -> { try { return device.getDeviceAbis(); } catch (Exception e) { throw new HumanReadableException("Unable to communicate with device", e); } }, pathResolver, projectFilesystem, exoInfo.getNativeLibsInfo().get()); installMissingFiles(presentFiles, nativeExoHelper.getFilesToInstall(), NATIVE_LIBRARY_TYPE); } if (exoInfo.getResourcesInfo().isPresent()) { ResourcesExoHelper resourcesExoHelper = new ResourcesExoHelper(pathResolver, projectFilesystem, exoInfo.getResourcesInfo().get()); installMissingFiles(presentFiles, resourcesExoHelper.getFilesToInstall(), RESOURCES_TYPE); } if (exoInfo.getModuleInfo().isPresent()) { ModuleExoHelper moduleExoHelper = new ModuleExoHelper(pathResolver, projectFilesystem, exoInfo.getModuleInfo().get()); installMissingFiles(presentFiles, moduleExoHelper.getFilesToInstall(), "modular_dex"); } } /** * @param apkInfo the apk info to examine for exopackage items * @return true if the given apk info contains any items which need to be installed via exopackage */ public static boolean exopackageEnabled(ApkInfo apkInfo) { return apkInfo.getExopackageInfo() .map(exoInfo -> exoInfo.getDexInfo().isPresent() || exoInfo.getNativeLibsInfo().isPresent() || exoInfo.getResourcesInfo().isPresent() || exoInfo.getModuleInfo().isPresent()) .orElse(false); } private Optional<PackageInfo> getPackageInfo(String packageName) throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, PerfEventId.of("get_package_info"), "package", packageName)) { return device.getPackageInfo(packageName); } } private boolean shouldAppBeInstalled(ApkInfo apkInfo) 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(pathResolver.getAbsolutePath(apkInfo.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(String packagePath) throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "get_app_signature")) { String output = device.getSignature(packagePath); String result = output.trim(); if (result.contains("\n") || result.contains("\r")) { throw new IllegalStateException("Unexpected return from get-signature:\n" + output); } return result; } } public void installMissingFiles(ImmutableSortedSet<Path> presentFiles, ImmutableMap<Path, Path> wantedFilesToInstall, String filesType) throws Exception { ImmutableSortedMap<Path, Path> filesToInstall = wantedFilesToInstall.entrySet().stream() .filter(entry -> !presentFiles.contains(entry.getKey())).collect(ImmutableSortedMap .toImmutableSortedMap(Ordering.natural(), Map.Entry::getKey, Map.Entry::getValue)); installFiles(filesType, filesToInstall); } private void deleteUnwantedFiles(ImmutableSortedSet<Path> presentFiles, ImmutableSet<Path> wantedFiles) { ImmutableSortedSet<Path> filesToDelete = presentFiles.stream() .filter(p -> !p.getFileName().toString().equals("lock") && !wantedFiles.contains(p)) .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural())); deleteFiles(filesToDelete); } private void deleteFiles(ImmutableSortedSet<Path> filesToDelete) { filesToDelete.stream() .collect(ImmutableListMultimap.toImmutableListMultimap(path -> dataRoot.resolve(path).getParent(), path -> path.getFileName().toString())) .asMap().forEach((dir, files) -> { device.rmFiles(dir.toString(), files); }); } private void installFiles(String filesType, ImmutableMap<Path, Path> filesToInstall) throws Exception { try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(eventBus, "multi_install_" + filesType); AutoCloseable ignored1 = device.createForward()) { // Make sure all the directories exist. filesToInstall.keySet().stream().map(p -> dataRoot.resolve(p).getParent()).distinct().forEach(p -> { try { device.mkDirP(p.toString()); } catch (Exception e) { throw new RuntimeException(e); } }); // Plan the installation. Map<Path, Path> installPaths = filesToInstall.entrySet().stream() .collect(Collectors.toMap(entry -> dataRoot.resolve(entry.getKey()), entry -> projectFilesystem.resolve(entry.getValue()))); // Install the files. device.installFiles(filesType, installPaths); } } private void installMetadata(ImmutableMap<Path, String> metadataToInstall) throws Exception { try (Closer closer = Closer.create()) { Map<Path, Path> filesToInstall = new HashMap<>(); for (Map.Entry<Path, String> entry : metadataToInstall.entrySet()) { NamedTemporaryFile temp = closer.register(new NamedTemporaryFile("metadata", "tmp")); com.google.common.io.Files.write(entry.getValue().getBytes(Charsets.UTF_8), temp.get().toFile()); filesToInstall.put(entry.getKey(), temp.get()); } installFiles("metadata", ImmutableMap.copyOf(filesToInstall)); } } /** * 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 public 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(); } }