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.AaptParser.AaptVersion; 24 import com.android.tradefed.util.CommandResult; 25 import com.android.tradefed.util.CommandStatus; 26 import com.android.tradefed.util.IRunUtil; 27 import com.android.tradefed.util.RunUtil; 28 29 import com.google.common.annotations.VisibleForTesting; 30 31 import java.io.IOException; 32 import java.nio.file.Path; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.concurrent.TimeUnit; 38 39 /** A utility class to install APKs. */ 40 public final class ApkInstaller { 41 private static long sCommandTimeOut = TimeUnit.MINUTES.toMillis(4); 42 private static long sObbPushCommandTimeOut = TimeUnit.MINUTES.toMillis(12); 43 private final String mDeviceSerial; 44 private final List<String> mInstalledPackages = new ArrayList<>(); 45 private final IRunUtil mRunUtil; 46 private final PackageNameParser mPackageNameParser; 47 getInstance(ITestDevice device)48 public static ApkInstaller getInstance(ITestDevice device) { 49 return getInstance(device.getSerialNumber()); 50 } 51 getInstance(String deviceSerial)52 public static ApkInstaller getInstance(String deviceSerial) { 53 return new ApkInstaller(deviceSerial, new RunUtil(), new AaptPackageNameParser()); 54 } 55 56 @VisibleForTesting ApkInstaller(String deviceSerial, IRunUtil runUtil, PackageNameParser packageNameParser)57 ApkInstaller(String deviceSerial, IRunUtil runUtil, PackageNameParser packageNameParser) { 58 mDeviceSerial = deviceSerial; 59 mRunUtil = runUtil; 60 mPackageNameParser = packageNameParser; 61 } 62 63 /** 64 * Installs a package. 65 * 66 * @param apkPath Path to the apk files. Only accept file/directory path containing a single APK 67 * or split APK files for one package. 68 * @param args Install args for the 'adb install-multiple' command. 69 * @throws ApkInstallerException If the installation failed. 70 * @throws IOException If an IO exception occurred. 71 */ install(Path apkPath, List<String> args)72 public void install(Path apkPath, List<String> args) throws ApkInstallerException, IOException { 73 List<Path> apkFilePaths; 74 try { 75 apkFilePaths = TestUtils.listApks(apkPath); 76 } catch (TestUtilsException e) { 77 throw new ApkInstallerException("Failed to list APK files from the path " + apkPath, e); 78 } 79 80 String packageName; 81 try { 82 packageName = mPackageNameParser.parsePackageName(apkFilePaths.get(0)); 83 } catch (IOException e) { 84 throw new ApkInstallerException( 85 String.format("Failed to parse the package name from %s", apkPath), e); 86 } 87 CLog.d("Attempting to uninstall package %s before installation", packageName); 88 String[] uninstallCmd = createUninstallCommand(packageName, mDeviceSerial); 89 // TODO(yuexima): Add command result checks after we start to check whether. 90 // the package is installed on device before uninstalling it. 91 // At this point, command failure is expected if the package wasn't installed. 92 mRunUtil.runTimedCmd(sCommandTimeOut, uninstallCmd); 93 94 CLog.d("Installing package %s from %s", packageName, apkPath); 95 96 String[] installApkCmd = createApkInstallCommand(apkFilePaths, mDeviceSerial, args); 97 98 CommandResult apkRes = mRunUtil.runTimedCmd(sCommandTimeOut, installApkCmd); 99 if (apkRes.getStatus() != CommandStatus.SUCCESS) { 100 throw new ApkInstallerException( 101 String.format( 102 "Failed to install APKs from the path %s: %s", 103 apkPath, apkRes.toString())); 104 } 105 106 mInstalledPackages.add(packageName); 107 108 List<String[]> installObbCmds = 109 createObbInstallCommands(apkFilePaths, mDeviceSerial, packageName); 110 for (String[] cmd : installObbCmds) { 111 CommandResult obbRes = mRunUtil.runTimedCmd(sObbPushCommandTimeOut, cmd); 112 if (obbRes.getStatus() != CommandStatus.SUCCESS) { 113 throw new ApkInstallerException( 114 String.format( 115 "Failed to install an OBB file from the path %s: %s", 116 apkPath, obbRes.toString())); 117 } 118 } 119 120 CLog.i("Successfully installed " + apkPath); 121 } 122 123 /** 124 * Overload for install method to use when install args are empty 125 * 126 * @param apkPath 127 * @throws ApkInstallerException 128 * @throws IOException 129 */ install(Path apkPath)130 public void install(Path apkPath) throws ApkInstallerException, IOException { 131 install(apkPath, Collections.emptyList()); 132 } 133 134 /** 135 * Installs apks from a list of paths. Can be used to install additional library apks or 3rd 136 * party apks. 137 * 138 * @param apkPaths List of paths to the apk files. 139 * @param args Install args for the 'adb install-multiple' command. 140 * @throws ApkInstallerException If the installation failed. 141 * @throws IOException If an IO exception occurred. 142 */ install(List<Path> apkPaths, List<String> args)143 public void install(List<Path> apkPaths, List<String> args) 144 throws ApkInstallerException, IOException { 145 for (Path apkPath : apkPaths) { 146 install(apkPath, args); 147 } 148 } 149 150 /** 151 * Attempts to uninstall all the installed packages. 152 * 153 * <p>When failed to uninstall one of the installed packages, this method will still attempt to 154 * uninstall all other packages before throwing an exception. 155 * 156 * @throws ApkInstallerException when failed to uninstall a package. 157 */ uninstallAllInstalledPackages()158 public void uninstallAllInstalledPackages() throws ApkInstallerException { 159 CLog.d("Uninstalling all installed packages."); 160 161 StringBuilder errorMessage = new StringBuilder(); 162 mInstalledPackages.forEach( 163 installedPackage -> { 164 String[] cmd = createUninstallCommand(installedPackage, mDeviceSerial); 165 CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd); 166 if (res.getStatus() != CommandStatus.SUCCESS) { 167 errorMessage.append( 168 String.format( 169 "Failed to uninstall package %s. Reason: %s.\n", 170 installedPackage, res.toString())); 171 } 172 }); 173 174 if (errorMessage.length() > 0) { 175 throw new ApkInstallerException(errorMessage.toString()); 176 } 177 } 178 createApkInstallCommand( List<Path> apkFilePaths, String deviceSerial, List<String> args)179 private String[] createApkInstallCommand( 180 List<Path> apkFilePaths, String deviceSerial, List<String> args) { 181 ArrayList<String> cmd = new ArrayList<>(); 182 cmd.addAll(Arrays.asList("adb", "-s", deviceSerial, "install-multiple")); 183 cmd.addAll(args); 184 185 apkFilePaths.stream() 186 .map(Path::toString) 187 .filter(path -> path.toLowerCase().endsWith(".apk")) 188 .forEach(cmd::add); 189 190 return cmd.toArray(new String[cmd.size()]); 191 } 192 createObbInstallCommands( List<Path> apkFilePaths, String deviceSerial, String packageName)193 private List<String[]> createObbInstallCommands( 194 List<Path> apkFilePaths, String deviceSerial, String packageName) { 195 ArrayList<String[]> cmds = new ArrayList<>(); 196 197 apkFilePaths.stream() 198 .filter(path -> path.toString().toLowerCase().endsWith(".obb")) 199 .forEach( 200 path -> { 201 String dest = 202 "/sdcard/Android/obb/" + packageName + "/" + path.getFileName(); 203 cmds.add( 204 new String[] { 205 "adb", "-s", deviceSerial, "shell", "rm", "-f", dest 206 }); 207 cmds.add( 208 new String[] { 209 "adb", "-s", deviceSerial, "push", path.toString(), dest 210 }); 211 }); 212 213 if (!cmds.isEmpty()) { 214 cmds.add( 215 0, 216 new String[] { 217 "adb", 218 "-s", 219 deviceSerial, 220 "shell", 221 "mkdir", 222 "-p", 223 "/sdcard/Android/obb/" + packageName 224 }); 225 } 226 227 return cmds; 228 } 229 createUninstallCommand(String packageName, String deviceSerial)230 private String[] createUninstallCommand(String packageName, String deviceSerial) { 231 List<String> cmd = Arrays.asList("adb", "-s", deviceSerial, "uninstall", packageName); 232 return cmd.toArray(new String[cmd.size()]); 233 } 234 235 /** An exception class representing ApkInstaller error. */ 236 public static final class ApkInstallerException extends Exception { 237 /** 238 * Constructs a new {@link ApkInstallerException} with a meaningful error message. 239 * 240 * @param message A error message describing the cause of the error. 241 */ ApkInstallerException(String message)242 private ApkInstallerException(String message) { 243 super(message); 244 } 245 246 /** 247 * Constructs a new {@link ApkInstallerException} with a meaningful error message, and a 248 * cause. 249 * 250 * @param message A detailed error message. 251 * @param cause A {@link Throwable} capturing the original cause of the {@link 252 * ApkInstallerException}. 253 */ ApkInstallerException(String message, Throwable cause)254 private ApkInstallerException(String message, Throwable cause) { 255 super(message, cause); 256 } 257 258 /** 259 * Constructs a new {@link ApkInstallerException} with a cause. 260 * 261 * @param cause A {@link Throwable} capturing the original cause of the {@link 262 * ApkInstallerException}. 263 */ ApkInstallerException(Throwable cause)264 private ApkInstallerException(Throwable cause) { 265 super(cause); 266 } 267 } 268 269 private static final class AaptPackageNameParser implements PackageNameParser { 270 @Override parsePackageName(Path apkFile)271 public String parsePackageName(Path apkFile) throws IOException { 272 String packageName = 273 AaptParser.parse(apkFile.toFile(), AaptVersion.AAPT2).getPackageName(); 274 if (packageName == null) { 275 throw new IOException( 276 String.format("Failed to parse package name with AAPT for %s", apkFile)); 277 } 278 return packageName; 279 } 280 } 281 282 @VisibleForTesting 283 interface PackageNameParser { parsePackageName(Path apkFile)284 String parsePackageName(Path apkFile) throws IOException; 285 } 286 } 287