• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.compatibility.common.tradefed.build.CompatibilityBuildHelper;
19 import com.android.compatibility.common.tradefed.util.DynamicConfigFileReader;
20 import com.android.compatibility.common.util.BusinessLogic;
21 import com.android.compatibility.common.util.BusinessLogicFactory;
22 import com.android.compatibility.common.util.FeatureUtil;
23 import com.android.compatibility.common.util.PropertyUtil;
24 import com.android.tradefed.build.IBuildInfo;
25 import com.android.tradefed.config.GlobalConfiguration;
26 import com.android.tradefed.config.Option;
27 import com.android.tradefed.config.OptionClass;
28 import com.android.tradefed.device.DeviceNotAvailableException;
29 import com.android.tradefed.device.ITestDevice;
30 import com.android.tradefed.device.NativeDevice;
31 import com.android.tradefed.device.contentprovider.ContentProviderHandler;
32 import com.android.tradefed.invoker.IInvocationContext;
33 import com.android.tradefed.invoker.TestInformation;
34 import com.android.tradefed.log.LogUtil.CLog;
35 import com.android.tradefed.result.error.DeviceErrorIdentifier;
36 import com.android.tradefed.result.error.InfraErrorIdentifier;
37 import com.android.tradefed.targetprep.BaseTargetPreparer;
38 import com.android.tradefed.targetprep.BuildError;
39 import com.android.tradefed.targetprep.TargetSetupError;
40 import com.android.tradefed.testtype.IAbi;
41 import com.android.tradefed.testtype.IAbiReceiver;
42 import com.android.tradefed.testtype.IInvocationContextReceiver;
43 import com.android.tradefed.testtype.suite.TestSuiteInfo;
44 import com.android.tradefed.util.FileUtil;
45 import com.android.tradefed.util.MultiMap;
46 import com.android.tradefed.util.RunUtil;
47 import com.android.tradefed.util.StreamUtil;
48 import com.android.tradefed.util.net.HttpHelper;
49 import com.android.tradefed.util.net.IHttpHelper;
50 
51 import com.google.api.client.auth.oauth2.Credential;
52 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
53 import com.google.common.annotations.VisibleForTesting;
54 import com.google.common.base.Strings;
55 
56 import org.json.JSONException;
57 import org.json.JSONObject;
58 import org.xmlpull.v1.XmlPullParserException;
59 
60 import java.io.DataOutputStream;
61 import java.io.File;
62 import java.io.FileInputStream;
63 import java.io.FileNotFoundException;
64 import java.io.IOException;
65 import java.net.HttpURLConnection;
66 import java.net.URL;
67 import java.util.ArrayList;
68 import java.util.Collections;
69 import java.util.Date;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.regex.Pattern;
73 import java.util.regex.Matcher;
74 import java.util.Set;
75 
76 /**
77  * Pushes business Logic to the host and the test device, for use by test cases in the test suite.
78  */
79 @OptionClass(alias = "business-logic-preparer")
80 public class BusinessLogicPreparer extends BaseTargetPreparer
81         implements IAbiReceiver, IInvocationContextReceiver {
82 
83     /* Placeholder in the service URL for the suite to be configured */
84     private static final String SUITE_PLACEHOLDER = "{suite-name}";
85 
86     /* String for the key to get file from GlobalConfiguration */
87     private static final String GLOBAL_APE_API_KEY = "ape-api-key";
88 
89     /* String for creating files to store the business logic configuration on the host */
90     private static final String FILE_LOCATION = "business-logic";
91     /* String for creating cached business logic configuration files */
92     private static final String BL_CACHE_FILE = "business-logic-cache";
93     /* Number of days for which cached business logic is valid */
94     private static final int BL_CACHE_DAYS = 5;
95     /* BL_CACHE_DAYS converted to millis */
96     private static final long BL_CACHE_MILLIS = BL_CACHE_DAYS * 1000 * 60 * 60 * 24L;
97     /* Extension of business logic files */
98     private static final String FILE_EXT = ".bl";
99     /* Default amount of time to attempt connection to the business logic service, in seconds */
100     private static final int DEFAULT_CONNECTION_TIME = 60;
101     /* Time to wait between connection attempts to the business logic service, in millis */
102     private static final long SLEEP_BETWEEN_CONNECTIONS_MS = 5000; // 5 seconds
103     /* Dynamic config constants */
104     private static final String DYNAMIC_CONFIG_FEATURES_KEY = "business_logic_device_features";
105     private static final String DYNAMIC_CONFIG_PROPERTIES_KEY = "business_logic_device_properties";
106     private static final String DYNAMIC_CONFIG_PACKAGES_KEY = "business_logic_device_packages";
107     private static final String DYNAMIC_CONFIG_EXTENDED_DEVICE_INFO_KEY =
108             "business_logic_extended_device_info";
109 
110     @Option(name = "business-logic-url", description = "The URL to use when accessing the " +
111             "business logic service, parameters not included", mandatory = true)
112     private String mUrl;
113 
114     @Option(name = "business-logic-api-key", description = "The API key to use when accessing " +
115             "the business logic service.", mandatory = true)
116     private String mApiKey;
117 
118     @Option(name = "business-logic-api-scope", description = "The URI of api scope to use when " +
119             "retrieving business logic rules.")
120     /* URI of api scope to use when retrieving business logic rules */
121     private  String mApiScope;
122 
123     @Option(name = "cache-business-logic", description = "Whether to keep and use cached " +
124             "business logic files.")
125     private boolean mCache = false;
126 
127     @Option(name = "clean-cache-business-logic", description = "Like option " +
128             "'cache-business-logic', but forces a refresh of the cached business logic file")
129     private boolean mCleanCache = false;
130 
131     @Option(name = "ignore-business-logic-failure", description = "Whether to proceed with the " +
132             "suite invocation if retrieval of business logic fails.")
133     private boolean mIgnoreFailure = false;
134 
135     @Option(name = "business-logic-connection-time", description = "Amount of time to attempt " +
136             "connection to the business logic service, in seconds.")
137     private int mMaxConnectionTime = DEFAULT_CONNECTION_TIME;
138 
139     @Option(name = "config-filename", description = "The module name for module-level " +
140             "configurations, or the suite name for suite-level configurations. Will lookup " +
141             "suite name if not provided.")
142     private String mModuleName = null;
143 
144     @Option(name = "version", description = "The module configuration version to retrieve.")
145     private String mModuleVersion = null;
146 
147     @Option(
148             name = "suite-version-extraction-regex",
149             description =
150                     "A regex string with a named capture group \"version\". Used to compare"
151                             + " versions on the BL server. To exclude a platform version name"
152                             + " prefix for example, use \".+?_sts(?<version>.+)\""
153                             + "('12.1_sts-r1' -> '-r1'). Note that <version> can be represented"
154                             + " in xml with &lt;version&gt;.")
155     private String mSuiteVersionExtractionRegex = "(?<version>.+)";
156 
157     private String mDeviceFilePushed;
158     private String mHostFilePushed;
159     private IAbi mAbi = null;
160     private IInvocationContext mModuleContext = null;
161 
162     /** {@inheritDoc} */
163     @Override
setAbi(IAbi abi)164     public void setAbi(IAbi abi) {
165         mAbi = abi;
166     }
167 
168     /** {@inheritDoc} */
169     @Override
getAbi()170     public IAbi getAbi() {
171         return mAbi;
172     }
173 
174 
175     /** {@inheritDoc} */
176     @Override
setInvocationContext(IInvocationContext invocationContext)177     public void setInvocationContext(IInvocationContext invocationContext) {
178         mModuleContext = invocationContext;
179     }
180 
181     /** {@inheritDoc} */
182     @Override
setUp(TestInformation testInfo)183     public void setUp(TestInformation testInfo)
184             throws TargetSetupError, BuildError, DeviceNotAvailableException {
185         IBuildInfo buildInfo = testInfo.getBuildInfo();
186         ITestDevice device = testInfo.getDevice();
187         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
188         if (buildHelper.hasBusinessLogicHostFile()) {
189             CLog.i("Business logic file already collected, skipping BusinessLogicPreparer.");
190             return;
191         }
192         // Ensure mModuleName is set.
193         if (mModuleName == null) {
194             mModuleName = "";
195             CLog.w("Option config-filename isn't set. Using empty string instead.");
196         }
197         if (mModuleVersion == null) {
198             CLog.w("Option version isn't set. Using 'null' instead.");
199             mModuleVersion = "null";
200         }
201         String requestParams = buildRequestParams(device, buildInfo);
202         String baseUrl = mUrl.replace(SUITE_PLACEHOLDER, getSuiteNames().get(0));
203         String businessLogicString = null;
204         // use cached business logic string if options are set accordingly and cache is valid,
205         // otherwise proceed with remote download.
206         if (!shouldReadCache()
207                 || (businessLogicString = readFromCache(baseUrl, requestParams)) == null) {
208             CLog.i("Attempting to connect to business logic service...");
209         }
210         long start = System.currentTimeMillis();
211         Exception connectIssue = null;
212         while (businessLogicString == null
213                 && System.currentTimeMillis() < (start + (mMaxConnectionTime * 1000))) {
214             try {
215                 businessLogicString = doPost(baseUrl, requestParams);
216             } catch (IOException e) {
217                 // ignore, re-attempt connection with remaining time
218                 CLog.d("BusinessLogic connection failure message: %s\nRetrying...", e.getMessage());
219                 connectIssue = e;
220                 RunUtil.getDefault().sleep(SLEEP_BETWEEN_CONNECTIONS_MS);
221             }
222         }
223         if (businessLogicString == null) {
224             if (mIgnoreFailure) {
225                 CLog.e("Failed to connect to business logic service.\nProceeding with test "
226                         + "invocation, tests depending on the remote configuration will fail.\n");
227                 return;
228             } else {
229                 String baseMessage =
230                         String.format(
231                                 "Cannot connect to business logic service for config %s. If this"
232                                         + " problem persists, re-invoking with option"
233                                         + " '--ignore-business-logic-failure' will cause tests to"
234                                         + " execute anyways (though tests depending on the remote"
235                                         + " configuration will fail).",
236                                 mModuleName);
237                 if (connectIssue != null) {
238                     baseMessage = String.format("%s.\n%s", connectIssue.getMessage(), baseMessage);
239                 }
240                 throw new TargetSetupError(
241                         baseMessage,
242                         device.getDeviceDescriptor(),
243                         InfraErrorIdentifier.ANDROID_PARTNER_SERVER_ERROR);
244             }
245         }
246 
247         if (shouldWriteCache()) {
248             writeToCache(businessLogicString, baseUrl, requestParams, mCleanCache);
249         }
250         // Push business logic string to host file
251         try {
252             File hostFile = FileUtil.createTempFile(FILE_LOCATION, FILE_EXT);
253             FileUtil.writeToFile(businessLogicString, hostFile);
254             mHostFilePushed = hostFile.getAbsolutePath();
255             // Ensure bitness is set.
256             String bitness = (mAbi != null) ? mAbi.getBitness() : "";
257             buildHelper.setBusinessLogicHostFile(hostFile, bitness + mModuleName);
258         } catch (IOException e) {
259             throw new TargetSetupError(
260                     String.format(
261                             "Retrieved business logic for config %s could not be written to host",
262                             mModuleName),
263                     device.getDeviceDescriptor(),
264                     InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
265         }
266         // Push business logic string to device file
267         removeDeviceFile(device); // remove any existing business logic file from device
268         if (device.pushString(businessLogicString, BusinessLogic.DEVICE_FILE)) {
269             mDeviceFilePushed = BusinessLogic.DEVICE_FILE;
270         } else {
271             throw new TargetSetupError(
272                     String.format(
273                             "Retrieved business logic for config %s could not be written to device"
274                                     + " %s",
275                             mModuleName, device.getSerialNumber()),
276                     device.getDeviceDescriptor(),
277                     DeviceErrorIdentifier.FAIL_PUSH_FILE);
278         }
279         checkAndInstallContentProvider(device);
280     }
281 
282     /** Helper to populate the business logic service request with info about the device. */
283     @VisibleForTesting
buildRequestParams(ITestDevice device, IBuildInfo buildInfo)284     String buildRequestParams(ITestDevice device, IBuildInfo buildInfo)
285             throws DeviceNotAvailableException, TargetSetupError {
286         MultiMap<String, String> paramMap = new MultiMap<>();
287         String suiteVersion = getSuiteVersionExtracted(buildInfo);
288         if (suiteVersion == null) {
289             suiteVersion = "null";
290         }
291         paramMap.put("suite_version", suiteVersion);
292         paramMap.put("module_version", mModuleVersion);
293         paramMap.put("oem", String.valueOf(PropertyUtil.getManufacturer(device)));
294         for (String feature : getBusinessLogicFeatures(device, buildInfo)) {
295             paramMap.put("features", feature);
296         }
297         for (String property : getBusinessLogicProperties(device, buildInfo)) {
298             paramMap.put("properties", property);
299         }
300         for (String pkg : getBusinessLogicPackages(device, buildInfo)) {
301             paramMap.put("packages", pkg);
302         }
303         for (String deviceInfo : getExtendedDeviceInfo(buildInfo)) {
304             paramMap.put("device_info", deviceInfo);
305         }
306         IHttpHelper helper = new HttpHelper();
307         String paramString = helper.buildParameters(paramMap);
308         CLog.d("Built param string: \"%s\"", paramString);
309         return paramString;
310     }
311 
312     /**
313      * Extract the version string we should use to compare versions on the BL server. Control what's
314      * extracted with the suite-version-extraction-regex option. This defaults to no changes to the
315      * original build. Suites that prepend the platform version name may use this to remove it.
316      */
317     @VisibleForTesting
getSuiteVersionExtracted(IBuildInfo buildInfo)318     String getSuiteVersionExtracted(IBuildInfo buildInfo) throws TargetSetupError {
319         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
320         String suiteVersion = buildHelper.getSuiteVersion();
321         if (suiteVersion == null) {
322             return null;
323         }
324         Matcher m = Pattern.compile(mSuiteVersionExtractionRegex).matcher(suiteVersion);
325         if (m.matches()) {
326             try {
327                 String extracted = m.group("version");
328                 CLog.d("original version: %s, extracted version: %s", suiteVersion, extracted);
329                 return extracted;
330             } catch (IllegalStateException | IllegalArgumentException e) {
331                 throw new TargetSetupError(
332                         String.format(
333                                 "Could not match the extraction regex (%s) against the suite"
334                                         + " version (%s)",
335                                 mSuiteVersionExtractionRegex, suiteVersion),
336                         e,
337                         InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
338             }
339         }
340         throw new TargetSetupError(
341                 String.format(
342                         "Could not match the extraction regex (%s) against the suite version (%s)",
343                         mSuiteVersionExtractionRegex, suiteVersion),
344                 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
345     }
346 
347     /**
348      * Return list of test-suite-tag from configuration if it's not empty,
349      * otherwise, return the name from test-suite-info.properties.
350      */
351     @VisibleForTesting
getSuiteNames()352     List<String> getSuiteNames() {
353         if (mModuleContext != null) {
354             List<String> testSuiteTags = mModuleContext.getConfigurationDescriptor().
355                     getSuiteTags();
356             if (!testSuiteTags.isEmpty()) {
357                 CLog.i("Adding %s from test suite tags to get value from dynamic config",
358                         testSuiteTags);
359                 return testSuiteTags;
360             }
361         }
362         String suiteName = TestSuiteInfo.getInstance().getName().toLowerCase();
363         CLog.i("Using %s from TestSuiteInfo to get value from dynamic config",
364                 suiteName);
365         return Collections.singletonList(suiteName);
366     }
367 
368     /**
369      * Check and install tradefed content provider.
370      *
371      * <p>Do nothing if the content provider is already installed, otherwise install it and
372      * initialize a {@code ContentProviderHandler} for the current user.
373      *
374      * <p>BusinessLogicTestCase relies on the tradefed content provider to read the BL config file
375      * successfully from the device side.
376      */
377     @VisibleForTesting
checkAndInstallContentProvider(ITestDevice device)378     static void checkAndInstallContentProvider(ITestDevice device)
379             throws DeviceNotAvailableException {
380         if (!device.isPackageInstalled(ContentProviderHandler.PACKAGE_NAME)) {
381             if (device instanceof NativeDevice) {
382                 var unused = ((NativeDevice) device).getContentProvider(device.getCurrentUser());
383             }
384         }
385     }
386 
387     /* Get device properties list, with element format "<property_name>:<property_value>" */
getBusinessLogicProperties(ITestDevice device, IBuildInfo buildInfo)388     private List<String> getBusinessLogicProperties(ITestDevice device, IBuildInfo buildInfo)
389             throws DeviceNotAvailableException {
390         List<String> properties = new ArrayList<>();
391         Map<String, String> clientIds = PropertyUtil.getClientIds(device);
392         for (Map.Entry<String, String> id : clientIds.entrySet()) {
393             // add client IDs to the list of properties
394             properties.add(String.format("%s:%s", id.getKey(), id.getValue()));
395         }
396 
397         try {
398             List<String> propertyNames = DynamicConfigFileReader.getValuesFromConfig(buildInfo,
399                     getSuiteNames(), DYNAMIC_CONFIG_PROPERTIES_KEY);
400             for (String name : propertyNames) {
401                 // Use String.valueOf in case property is undefined for the device ("null")
402                 String value = String.valueOf(device.getProperty(name));
403                 properties.add(String.format("%s:%s", name, value));
404             }
405         } catch (XmlPullParserException | IOException e) {
406             CLog.e("Failed to pull business logic properties from dynamic config");
407         }
408         return properties;
409     }
410 
411     /* Get device features list */
getBusinessLogicFeatures(ITestDevice device, IBuildInfo buildInfo)412     private List<String> getBusinessLogicFeatures(ITestDevice device, IBuildInfo buildInfo)
413             throws DeviceNotAvailableException {
414         try {
415             List<String> dynamicConfigFeatures = DynamicConfigFileReader.getValuesFromConfig(
416                     buildInfo, getSuiteNames(), DYNAMIC_CONFIG_FEATURES_KEY);
417             Set<String> deviceFeatures = FeatureUtil.getAllFeatures(device);
418             dynamicConfigFeatures.retainAll(deviceFeatures);
419             return dynamicConfigFeatures;
420         } catch (XmlPullParserException | IOException e) {
421             CLog.e("Failed to pull business logic features from dynamic config");
422             return new ArrayList<>();
423         }
424     }
425 
426     /* Get device packages list */
getBusinessLogicPackages(ITestDevice device, IBuildInfo buildInfo)427     private List<String> getBusinessLogicPackages(ITestDevice device, IBuildInfo buildInfo)
428             throws DeviceNotAvailableException {
429         try {
430             List<String> dynamicConfigPackages = DynamicConfigFileReader.getValuesFromConfig(
431                     buildInfo, getSuiteNames(), DYNAMIC_CONFIG_PACKAGES_KEY);
432             Set<String> devicePackages = device.getInstalledPackageNames();
433             dynamicConfigPackages.retainAll(devicePackages);
434             return dynamicConfigPackages;
435         } catch (XmlPullParserException | IOException e) {
436             CLog.e("Failed to pull business logic packages from dynamic config");
437             return new ArrayList<>();
438         }
439     }
440 
441     /* Get extended device info*/
getExtendedDeviceInfo(IBuildInfo buildInfo)442     private List<String> getExtendedDeviceInfo(IBuildInfo buildInfo) {
443         List<String> extendedDeviceInfo = new ArrayList<>();
444         File deviceInfoPath = buildInfo.getFile(DeviceInfoCollector.DEVICE_INFO_DIR);
445         if (deviceInfoPath == null || !deviceInfoPath.exists()) {
446             CLog.w("Device Info directory was not created (Make sure you are not running plan " +
447                     "\"*ts-dev\" or including option -d/--skip-device-info)");
448             return extendedDeviceInfo;
449         }
450         List<String> requiredDeviceInfo = null;
451         try {
452             requiredDeviceInfo = DynamicConfigFileReader.getValuesFromConfig(
453                 buildInfo, getSuiteNames(), DYNAMIC_CONFIG_EXTENDED_DEVICE_INFO_KEY);
454         } catch (XmlPullParserException | IOException e) {
455             CLog.e("Failed to pull business logic Extended DeviceInfo from dynamic config. "
456                 + "Error: %s", e);
457             return extendedDeviceInfo;
458         }
459         File ediFile = null;
460         String[] fileAndKey = null;
461         try{
462             for (String ediEntry: requiredDeviceInfo) {
463                 fileAndKey = ediEntry.split(":");
464                 if (fileAndKey.length <= 1) {
465                     CLog.e("Dynamic config Extended DeviceInfo key has problem.");
466                     return new ArrayList<>();
467                 }
468                 ediFile = FileUtil
469                     .findFile(deviceInfoPath, fileAndKey[0] + ".deviceinfo.json");
470                 if (ediFile == null) {
471                     CLog.e(
472                             "Could not find Extended DeviceInfo JSON file: %s.",
473                             deviceInfoPath + fileAndKey[0] + ".deviceinfo.json");
474                     return new ArrayList<>();
475                 }
476                 String jsonString = FileUtil.readStringFromFile(ediFile);
477                 JSONObject jsonObj = new JSONObject(jsonString);
478                 String value = jsonObj.getString(fileAndKey[1]);
479                 extendedDeviceInfo
480                     .add(String.format("%s:%s:%s", fileAndKey[0], fileAndKey[1], value));
481             }
482         }catch(JSONException | IOException | RuntimeException e){
483             CLog.e(
484                     "Failed to read or parse Extended DeviceInfo JSON file: %s. Error: %s",
485                     deviceInfoPath + fileAndKey[0] + ".deviceinfo.json", e);
486             return new ArrayList<>();
487         }
488         return extendedDeviceInfo;
489     }
490 
shouldReadCache()491     private boolean shouldReadCache() {
492         return mCache && !mCleanCache;
493     }
494 
shouldWriteCache()495     private boolean shouldWriteCache() {
496         return mCache || mCleanCache;
497     }
498 
499     /**
500      * Read the string from the business logic cache, handling the following cases with a null
501      * return value:
502      * - The cached file does not exist
503      * - The cached file cannot be read
504      * - The cached file is timestamped more than BL_CACHE_DAYS prior to now
505      * In the last two cases, the file is deleted so an up-to-date configuration may be cached anew
506      */
readFromCache(String baseUrl, String params)507     private static synchronized String readFromCache(String baseUrl, String params) {
508         // baseUrl + params hashCode makes file unique, in case host runs invocations for different
509         // device builds and/or test suites using business logic
510         File cachedFile = getCachedFile(baseUrl, params);
511         if (!cachedFile.exists()) {
512             CLog.i("No cached business logic found");
513             return null;
514         }
515         try {
516             BusinessLogic cachedLogic = BusinessLogicFactory.createFromFile(cachedFile);
517             Date cachedDate = cachedLogic.getTimestamp();
518             if (System.currentTimeMillis() - cachedDate.getTime() < BL_CACHE_MILLIS) {
519                 CLog.i("Using cached business logic from: %s", cachedDate.toString());
520                 return FileUtil.readStringFromFile(cachedFile);
521             } else {
522                 CLog.i("Cached business logic out-of-date, deleting cached file");
523                 FileUtil.deleteFile(cachedFile);
524             }
525         } catch (IOException e) {
526             CLog.w("Failed to read cached business logic, deleting cached file");
527             FileUtil.deleteFile(cachedFile);
528         }
529         return null;
530     }
531 
532     /**
533      * Write a string retrieved from the business logic service to the cache file, only if the
534      * file does not already exist. Synchronize this method to prevent concurrent writes in the
535      * sharding case.
536      * @param blString the string to cache
537      * @param baseUrl the base business logic request url containing suite info
538      * @param params the string of params for the business logic request containing device info
539      */
writeToCache(String blString, String baseUrl, String params, boolean overwrite)540     private static synchronized void writeToCache(String blString, String baseUrl, String params,
541             boolean overwrite) {
542         // baseUrl + params hashCode makes file unique, in case host runs invocations for different
543         // device builds and/or test suites using business logic
544         File cachedFile = getCachedFile(baseUrl, params);
545         if (!cachedFile.exists() || overwrite) {
546             // don't overwrite existing file, whether from previous shard or previous invocation
547             try {
548                 FileUtil.writeToFile(blString, cachedFile);
549             } catch (IOException e) {
550                 throw new RuntimeException("Failed to write business logic to cache file", e);
551             }
552         }
553     }
554 
555     /**
556      * Get the cached business logic file given the base url and params used to retrieve this logic.
557      */
getCachedFile(String baseUrl, String params)558     private static File getCachedFile(String baseUrl, String params) {
559         int hashCode = (baseUrl + params).hashCode();
560         return new File(System.getProperty("java.io.tmpdir"), BL_CACHE_FILE + hashCode);
561     }
562 
doPost(String baseUrl, String params)563     private String doPost(String baseUrl, String params) throws IOException {
564         String accessToken = getToken();
565         if (Strings.isNullOrEmpty(accessToken)) {
566             // Set API key on base URL
567             baseUrl += String.format("?key=%s", mApiKey);
568         }
569         URL url = new URL(baseUrl);
570         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
571         conn.setRequestMethod("POST");
572         conn.setRequestProperty("User-Agent", "BusinessLogicClient");
573         if (!Strings.isNullOrEmpty(accessToken)) {
574             // Set authorization access token in POST header
575             conn.setRequestProperty("Authorization", String.format("Bearer %s", accessToken));
576         }
577         // Send params in POST request body
578         conn.setDoOutput(true);
579         try (DataOutputStream wr = new DataOutputStream(conn.getOutputStream())) {
580             wr.writeBytes(params);
581         }
582         int responseCode = conn.getResponseCode();
583         CLog.d("Business Logic Service Response Code : %s", responseCode);
584         return StreamUtil.getStringFromStream(conn.getInputStream());
585     }
586 
587     /** {@inheritDoc} */
588     @Override
tearDown(TestInformation testInfo, Throwable e)589     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
590         // Clean up existing host and device files unconditionally
591         if (mHostFilePushed != null) {
592             FileUtil.deleteFile(new File(mHostFilePushed));
593         }
594         if (mDeviceFilePushed != null && !(e instanceof DeviceNotAvailableException)) {
595             removeDeviceFile(testInfo.getDevice());
596         }
597     }
598 
599     /** Remove business logic file from the device */
removeDeviceFile(ITestDevice device)600     private static void removeDeviceFile(ITestDevice device) throws DeviceNotAvailableException {
601         device.deleteFile(BusinessLogic.DEVICE_FILE);
602     }
603 
604     /**
605      * Returns an OAuth2 token string obtained using a service account json key file.
606      *
607      * Uses the service account key file location stored in environment variable 'APE_API_KEY'
608      * to request an OAuth2 token. If APE_API_KEY wasn't set, try to get if file is dynamically
609      * downloaded from GlobalConfiguration.
610      */
getToken()611     private String getToken() {
612         String keyFilePath = System.getenv("APE_API_KEY");
613         if (Strings.isNullOrEmpty(keyFilePath)) {
614             File globalKeyFile = GlobalConfiguration.getInstance().getHostOptions().
615                 getServiceAccountJsonKeyFiles().get(GLOBAL_APE_API_KEY);
616             if (globalKeyFile == null || !globalKeyFile.exists()) {
617                 CLog.d("Unable to fetch the service key because neither environment variable " +
618                         "APE_API_KEY is set nor the key file is dynamically downloaded.");
619                 return null;
620             }
621             keyFilePath = globalKeyFile.getAbsolutePath();
622         }
623         if (Strings.isNullOrEmpty(mApiScope)) {
624             CLog.d("API scope not set, use flag --business-logic-api-scope.");
625             return null;
626         }
627         try {
628             Credential credential = GoogleCredential.fromStream(new FileInputStream(keyFilePath))
629                     .createScoped(Collections.singleton(mApiScope));
630             credential.refreshToken();
631             return credential.getAccessToken();
632         } catch (FileNotFoundException e) {
633             CLog.e(String.format("Service key file %s doesn't exist.", keyFilePath));
634         } catch (IOException e) {
635             CLog.e(String.format("Can't read the service key file, %s", keyFilePath));
636         }
637         return null;
638     }
639 }
640