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