1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.csuite.core; 18 19 import com.android.csuite.core.TestUtils.TestUtilsException; 20 import com.android.tradefed.device.ITestDevice; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.util.AaptParser; 23 import com.android.tradefed.util.CommandResult; 24 import com.android.tradefed.util.CommandStatus; 25 import com.android.tradefed.util.IRunUtil; 26 import com.android.tradefed.util.RunUtil; 27 28 import com.google.common.annotations.VisibleForTesting; 29 30 import java.io.IOException; 31 import java.nio.file.Path; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.List; 35 import java.util.concurrent.TimeUnit; 36 37 /** A utility class to install APKs. */ 38 public final class ApkInstaller { 39 private static long sCommandTimeOut = TimeUnit.MINUTES.toMillis(4); 40 private final String mDeviceSerial; 41 private final List<Path> mInstalledBaseApks = new ArrayList<>(); 42 private final IRunUtil mRunUtil; 43 private final PackageNameParser mPackageNameParser; 44 getInstance(ITestDevice device)45 public static ApkInstaller getInstance(ITestDevice device) { 46 return getInstance(device.getSerialNumber()); 47 } 48 getInstance(String deviceSerial)49 public static ApkInstaller getInstance(String deviceSerial) { 50 return new ApkInstaller(deviceSerial, new RunUtil(), new AaptPackageNameParser()); 51 } 52 53 @VisibleForTesting ApkInstaller(String deviceSerial, IRunUtil runUtil, PackageNameParser packageNameParser)54 ApkInstaller(String deviceSerial, IRunUtil runUtil, PackageNameParser packageNameParser) { 55 mDeviceSerial = deviceSerial; 56 mRunUtil = runUtil; 57 mPackageNameParser = packageNameParser; 58 } 59 60 /** 61 * Installs a package. 62 * 63 * @param apkPath Path to the apk files. Only accept file/directory path containing a single APK 64 * or split APK files for one package. 65 * @param args Install args for the 'adb install-multiple' command. 66 * @throws ApkInstallerException If the installation failed. 67 * @throws IOException If an IO exception occurred. 68 */ install(Path apkPath, String... args)69 public void install(Path apkPath, String... args) throws ApkInstallerException, IOException { 70 List<Path> apkFilePaths; 71 try { 72 apkFilePaths = TestUtils.listApks(apkPath); 73 } catch (TestUtilsException e) { 74 throw new ApkInstallerException("Failed to list APK files from the path " + apkPath, e); 75 } 76 77 CLog.d("Installing a package from " + apkPath); 78 79 String[] cmd = createInstallCommand(apkFilePaths, mDeviceSerial, args); 80 81 CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd); 82 if (res.getStatus() != CommandStatus.SUCCESS) { 83 throw new ApkInstallerException( 84 String.format( 85 "Failed to install APKs from the path %s: %s", 86 apkPath, res.toString())); 87 } 88 89 mInstalledBaseApks.add(apkFilePaths.get(0)); 90 91 CLog.i("Successfully installed " + apkPath); 92 } 93 94 /** 95 * Attempts to uninstall all the installed packages. 96 * 97 * <p>When failed to uninstall one of the installed packages, this method will still attempt to 98 * uninstall all other packages before throwing an exception. 99 * 100 * @throws ApkInstallerException when failed to uninstall a package. 101 */ uninstallAllInstalledPackages()102 public void uninstallAllInstalledPackages() throws ApkInstallerException { 103 StringBuilder errorMessage = new StringBuilder(); 104 mInstalledBaseApks.forEach( 105 baseApk -> { 106 String packageName; 107 try { 108 packageName = mPackageNameParser.parsePackageName(baseApk); 109 } catch (IOException e) { 110 errorMessage.append( 111 String.format( 112 "Failed to parse the package name from %s. Reason: %s.\n", 113 baseApk, e.getMessage())); 114 return; 115 } 116 117 String[] cmd = 118 new String[] {"adb", "-s", mDeviceSerial, "uninstall", packageName}; 119 120 CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd); 121 if (res.getStatus() != CommandStatus.SUCCESS) { 122 errorMessage.append( 123 String.format( 124 "Failed to uninstall package %s from %s. Reason: %s.\n", 125 packageName, baseApk, res.toString())); 126 } 127 }); 128 129 if (errorMessage.length() > 0) { 130 throw new ApkInstallerException(errorMessage.toString()); 131 } 132 } 133 createInstallCommand( List<Path> apkFilePaths, String deviceSerial, String[] args)134 private String[] createInstallCommand( 135 List<Path> apkFilePaths, String deviceSerial, String[] args) { 136 ArrayList<String> cmd = new ArrayList<>(); 137 cmd.addAll(Arrays.asList("adb", "-s", deviceSerial, "install-multiple")); 138 139 cmd.addAll(Arrays.asList(args)); 140 141 apkFilePaths.stream().map(Path::toString).forEach(cmd::add); 142 143 return cmd.toArray(new String[cmd.size()]); 144 } 145 146 /** An exception class representing ApkInstaller error. */ 147 public static final class ApkInstallerException extends Exception { 148 /** 149 * Constructs a new {@link ApkInstallerException} with a meaningful error message. 150 * 151 * @param message A error message describing the cause of the error. 152 */ ApkInstallerException(String message)153 private ApkInstallerException(String message) { 154 super(message); 155 } 156 157 /** 158 * Constructs a new {@link ApkInstallerException} with a meaningful error message, and a 159 * cause. 160 * 161 * @param message A detailed error message. 162 * @param cause A {@link Throwable} capturing the original cause of the {@link 163 * ApkInstallerException}. 164 */ ApkInstallerException(String message, Throwable cause)165 private ApkInstallerException(String message, Throwable cause) { 166 super(message, cause); 167 } 168 169 /** 170 * Constructs a new {@link ApkInstallerException} with a cause. 171 * 172 * @param cause A {@link Throwable} capturing the original cause of the {@link 173 * ApkInstallerException}. 174 */ ApkInstallerException(Throwable cause)175 private ApkInstallerException(Throwable cause) { 176 super(cause); 177 } 178 } 179 180 private static final class AaptPackageNameParser implements PackageNameParser { 181 @Override parsePackageName(Path apkFile)182 public String parsePackageName(Path apkFile) throws IOException { 183 String packageName = AaptParser.parse(apkFile.toFile()).getPackageName(); 184 if (packageName == null) { 185 throw new IOException( 186 String.format("Failed to parse package name with AAPT for %s", apkFile)); 187 } 188 return packageName; 189 } 190 } 191 192 @VisibleForTesting 193 interface PackageNameParser { parsePackageName(Path apkFile)194 String parsePackageName(Path apkFile) throws IOException; 195 } 196 } 197