1 /*
2  * Copyright (C) 2019 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 package com.android.compatibility.common.tradefed.targetprep;
17 
18 import com.android.ddmlib.Log.LogLevel;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.config.Option;
21 import com.android.tradefed.config.OptionClass;
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.ITestDevice;
24 import com.android.tradefed.invoker.IInvocationContext;
25 import com.android.tradefed.invoker.InvocationContext;
26 import com.android.tradefed.invoker.TestInformation;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.targetprep.BaseTargetPreparer;
29 import com.android.tradefed.targetprep.BuildError;
30 import com.android.tradefed.targetprep.TargetSetupError;
31 import com.android.tradefed.util.AaptParser;
32 
33 import java.io.File;
34 import java.io.FileNotFoundException;
35 import java.util.ArrayList;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.Set;
39 
40 /**
41  * Installs device interaction helper implementations on the target.
42  *
43  * <p>Device interaction helpers allow xTS tests to drive aspects of a device that are not covered
44  * by public APIs but are necessary to make a test work, such as clicking a button to initiate
45  * printing in a test that exercises printing APIs. xTS tests running on the device will load
46  * concrete implementations at runtime using {@link HelperManager}, with a fallback to default
47  * implementations that interact with the default Android UI. See {@link
48  * DeviceInteractionHelperRule} for an example of connecting helpers to a test at runtime.
49  *
50  * <p>If device-specific implementations are needed, the device must contain a property listing the
51  * packages that will be searched at runtime. DeviceInteractionHelperInstaller will read this same
52  * property to determine which packages it will install. The default/fallback helpers are always
53  * installed.
54  *
55  * <p>See cts/libs/helpers and cts/helpers for more details and examples of device interaction
56  * helpers..
57  */
58 @OptionClass(alias = "device-interaction-helper", global_namespace = false)
59 public class DeviceInteractionHelperInstaller extends BaseTargetPreparer {
60 
61     @Option(
62             name = "default-package",
63             description = "name of the package containing fallback device interaction helpers")
64     private String mDefaultHelperPackage = "com.android.cts.helpers.aosp";
65 
66     @Option(
67             name = "property-name",
68             description = "name of a device property listing necessary OEM helper packages")
69     private String mHelperPackagePropertyKey = "ro.vendor.cts_interaction_helper_packages";
70 
71     private Set<String> mInstalledPackages = new HashSet<String>();
72 
73     /** {@inheritDoc} */
74     @Override
setUp(ITestDevice device, IBuildInfo buildInfo)75     public void setUp(ITestDevice device, IBuildInfo buildInfo)
76             throws TargetSetupError, BuildError, DeviceNotAvailableException {
77         IInvocationContext context = new InvocationContext();
78         context.addAllocatedDevice("device", device);
79         context.addDeviceBuildInfo("device", buildInfo);
80         TestInformation testInfo =
81                 TestInformation.newBuilder().setInvocationContext(context).build();
82         setUp(testInfo);
83     }
84 
85     /** {@inheritDoc} */
86     @Override
setUp(TestInformation testInfo)87     public void setUp(TestInformation testInfo)
88             throws TargetSetupError, BuildError, DeviceNotAvailableException {
89         ITestDevice device = testInfo.getDevice();
90 
91         // Default helpers are always required.  Push them first so we can fail early if missing.
92         String defaultHelperName = mDefaultHelperPackage + ".apk";
93         try {
94             File defaultHelpers = testInfo.getDependencyFile(defaultHelperName, true);
95             installHelperApk(device, defaultHelpers, mDefaultHelperPackage);
96         } catch (FileNotFoundException e) {
97             throw new BuildError(
98                     "Unable to find "
99                             + defaultHelperName
100                             + ". Make sure the file is present in your testcases directory or set "
101                             + " an explicit path with default-interaction-helper-package.",
102                     device.getDeviceDescriptor());
103         }
104 
105         // Parse the search path from the device property .  It is not an error if the property is
106         // empty.  This just means that the default helpers installed above will be used.
107         String searchPath = device.getProperty(mHelperPackagePropertyKey);
108         if (searchPath == null || searchPath.isEmpty()) {
109             return;
110         }
111 
112         // Search locally for each package listed in the device package search property and install
113         // everything found.  If a requested package is not found, that is not a fatal error because
114         // the particular test being run may not have required helpers from that package anyway.
115         for (String pkg : searchPath.split(":")) {
116             if (pkg.isEmpty()) {
117                 continue;
118             }
119             if (mInstalledPackages.contains(pkg)) {
120                 continue; // Already installed.
121             }
122 
123             try {
124                 String apkName = pkg + ".apk";
125                 File apkFile = testInfo.getDependencyFile(apkName, true);
126                 checkApkFile(device, apkFile, pkg);
127                 installHelperApk(device, apkFile, pkg);
128             } catch (FileNotFoundException e) {
129                 CLog.w("Unable to find apk for %s", pkg);
130             }
131         }
132     }
133 
134     /** {@inheritDoc} */
135     @Override
tearDown(TestInformation testInfo, Throwable t)136     public void tearDown(TestInformation testInfo, Throwable t) throws DeviceNotAvailableException {
137         ITestDevice device = testInfo.getDevice();
138         for (String pkg : mInstalledPackages) {
139             String msg = device.uninstallPackage(pkg);
140             if (msg != null) {
141                 CLog.w(String.format("Error uninstalling package '%s': %s", pkg, msg));
142             }
143         }
144     }
145 
146     /**
147      * Checks that an apk is readable and contains a valid package name.
148      *
149      * @param device the ITestDevice where the apk will be installed.
150      * @param apkFile the apk file to check.
151      * @param expectedPackage the expected name of the package in the apk.
152      * @throws BuildError if the apk isn't readable or isn't valid.
153      */
checkApkFile(ITestDevice device, File apkFile, String expectedPackage)154     private void checkApkFile(ITestDevice device, File apkFile, String expectedPackage)
155             throws BuildError {
156         String path = apkFile.getPath();
157         if (!apkFile.canRead()) {
158             throw new BuildError(
159                     "Helper " + path + " does not exist or is unreadable.",
160                     device.getDeviceDescriptor());
161         }
162         AaptParser parser = parseApk(apkFile);
163         if (parser == null) {
164             throw new BuildError(
165                     "Unable to parse helper apk " + path, device.getDeviceDescriptor());
166         }
167         String apkPackage = parser.getPackageName();
168         if (apkPackage == null || apkPackage.isEmpty()) {
169             throw new BuildError(
170                     "Unable to parse helper apk " + path, device.getDeviceDescriptor());
171         }
172         if (!expectedPackage.equals(apkPackage)) {
173             throw new BuildError(
174                     String.format(
175                             "Helper apk %s declares package %s but was expected to declare %s",
176                             path, apkPackage, expectedPackage),
177                     device.getDeviceDescriptor());
178         }
179     }
180 
parseApk(File apkFile)181     protected AaptParser parseApk(File apkFile) {
182         return AaptParser.parse(apkFile);
183     }
184 
185     /** Installs an apk containing DeviceInteractionHelper implementations. */
installHelperApk(ITestDevice device, File apkFile, String apkPackage)186     private void installHelperApk(ITestDevice device, File apkFile, String apkPackage)
187             throws BuildError, DeviceNotAvailableException, TargetSetupError {
188         if (apkFile == null) {
189             throw new BuildError("Invalid apk name", device.getDeviceDescriptor());
190         }
191         CLog.logAndDisplay(
192                 LogLevel.INFO, "Installing %s from %s", apkFile.getName(), apkFile.getPath());
193         List<String> extraArgs = new ArrayList<String>();
194         if (device.isAppEnumerationSupported()) {
195             extraArgs.add("--force-queryable");
196         }
197         String msg = device.installPackage(apkFile, true, extraArgs.toArray(new String[] {}));
198         if (msg != null) {
199             throw new TargetSetupError(
200                     String.format("Failed to install %s: %s", apkFile.getName(), msg),
201                     device.getDeviceDescriptor());
202         }
203         mInstalledPackages.add(apkPackage);
204     }
205 }
206