• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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