blob: 5a868ce2f7e29bc01fe9235d5e22b8d92271d394 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.tradefed.targetprep;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil;
import com.android.tradefed.targetprep.BuildError;
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.ZipUtil;
import java.awt.Dimension;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipFile;
/**
* A {@link ITargetPreparer} that performs steps on the host-side to meet the preconditions of CTS.
* <p/>
* This class is intended for runs of CTS against a device running a user build.
* <p/>
* This class performs checks to verify that the location services are on, WiFi is connected,
* the device locale is set to 'en-US', and that the device runs a user build. The class also
* performs automation to ensure that 3rd party app installs are enabled, the 'Stay Awake' setting
* is turned on, and that the appropriate media files are pushed to the device for the media tests.
* Additionally, options are provided for automatically connecting to a specific WiFi network.
*/
@OptionClass(alias="host-precondition-preparer")
public class HostPreconditionPreparer implements ITargetPreparer {
/* This option also exists in the DevicePreconditionPreparer */
@Option(name = "skip-preconditions",
description = "Whether to skip precondition checks and automation")
protected boolean mSkipPreconditions = false;
@Option(name = "wifi-ssid", description = "Name of the WiFi network with which to connect")
protected String mWifiSsid = null;
@Option(name = "wifi-psk", description = "The WPA-PSK associated with option 'wifi-ssid'")
protected String mWifiPsk = null;
@Option(name = "skip-media-download",
description = "Whether to skip verifying/downloading media files")
protected boolean mSkipMediaDownload = false;
@Option(name = "local-media-path",
description = "Absolute path of the media files directory on the host, containing" +
"'bbb_short' and 'bbb_full' directories")
protected String mLocalMediaPath = null;
private static final String LOG_TAG = HostPreconditionPreparer.class.getSimpleName();
/* Constants found in android.provider.Settings */
protected static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps";
protected static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in";
protected static final String PACKAGE_VERIFIER_INCLUDE_ADB = "verifier_verify_adb_installs";
protected static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed";
/* Constant from android.os.BatteryManager */
private static final int BATTERY_PLUGGED_USB = 2;
/* Name and expected value of the device's locale property */
private static final String LOCALE_PROPERTY_STRING = "ro.product.locale";
private static final String US_EN_LOCALE_STRING = "en-US";
/* Name and expected value of the device's build type property */
private static final String BUILD_TYPE_PROPERTY_STRING = "ro.build.type";
private static final String USER_BUILD_STRING = "user";
/* Logged if the preparer fails to identify the device's maximum video playback resolution */
private static final String MAX_PLAYBACK_RES_FAILURE_MSG =
"Unable to parse maximum video playback resolution, pushing all media files";
/*
* The URL from which to download the compressed media files
* TODO: Find a way to retrieve this programmatically
*/
private static final String MEDIA_URL_STRING =
"https://dl.google.com/dl/android/cts/android-cts-media-1.1.zip";
/*
* A default name for the local directory into which media files will be downloaded, if option
* "local-media-path" is not provided. This name is intentionally predetermined and final, so
* that when running CTS repeatedly, media files downloaded to the host in a previous run of
* CTS can be found in this directory, which will live inside the local temp directory.
*/
private static final String MEDIA_FOLDER_NAME = "android-cts-media";
/* Constants identifying video playback resolutions of the media files to be copied */
protected static final int RES_176_144 = 0; // 176x144 resolution
protected static final int RES_DEFAULT = 1; // 480x360, the default max playback resolution
protected static final int RES_720_480 = 2; // 720x480 resolution
protected static final int RES_1280_720 = 3; // 1280x720 resolution
protected static final int RES_1920_1080 = 4; // 1920x1080 resolution
/* Array of Dimensions aligning with and corresponding to the resolution constants above */
protected static final Dimension[] resolutions = {
new Dimension(176, 144),
new Dimension(480, 360),
new Dimension(720, 480),
new Dimension(1280, 720),
new Dimension(1920, 1080)
};
/*********************************************************************************************
* HELPER METHODS
*********************************************************************************************/
/* Helper that logs a message with LogLevel.INFO */
private static void printInfo(String msg) {
LogUtil.printLog(Log.LogLevel.INFO, LOG_TAG, msg);
}
/* Helper that logs a message with LogLevel.WARN */
private static void printWarning(String msg) {
LogUtil.printLog(Log.LogLevel.WARN, LOG_TAG, msg);
}
/*
* Returns a string representation of the dimension
* For dimension of width = 480 and height = 360, the resolution string is "480x360"
*/
protected static String resolutionString(Dimension resolution) {
return String.format("%dx%d", resolution.width, resolution.height);
}
/*
* Returns the device's absolute path to the directory containing 'short' media files, given
* a resolution. The instance of ITestDevice is used to identify the mount point for
* external storage.
*/
protected String getDeviceShortDir(ITestDevice device, Dimension resolution) {
String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
return String.format("%s/test/bbb_short/%s", mountPoint, resolutionString(resolution));
}
/*
* Returns the device's absolute path to the directory containing 'full' media files, given
* a resolution. The instance of ITestDevice is used to identify the mount point for
* external storage.
*/
protected String getDeviceFullDir(ITestDevice device, Dimension resolution) {
String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
return String.format("%s/test/bbb_full/%s", mountPoint, resolutionString(resolution));
}
/*
* Loops through the predefined maximum video playback resolutions from largest to smallest,
* And returns the greatest resolution that is strictly smaller than the width and height
* provided in the arguments.
*/
private Dimension getMaxVideoPlaybackResolution(int width, int height) {
for (int resIndex = resolutions.length - 1; resIndex >= RES_DEFAULT; resIndex--) {
Dimension resolution = resolutions[resIndex];
if (width >= resolution.width && height >= resolution.height) {
return resolution;
}
}
return resolutions[RES_DEFAULT];
}
/**
* Returns the maximum video playback resolution of the device, in the form of a Dimension
* object. This method parses dumpsys output to find resolutions listed under the
* 'mBaseDisplayInfo' field. The value of the 'smallest app' field is used as an estimate for
* maximum video playback resolution, and is rounded down to the nearest dimension in the
* resolutions array.
*/
protected Dimension getMaxVideoPlaybackResolution(ITestDevice device)
throws DeviceNotAvailableException{
String dumpsysOutput =
device.executeShellCommand("dumpsys display | grep mBaseDisplayInfo");
Pattern pattern = Pattern.compile("smallest app (\\d+) x (\\d+)");
Matcher matcher = pattern.matcher(dumpsysOutput);
if(!matcher.find()) {
// could not find resolution in dumpsysOutput, return largest max playback resolution
// so that preparer copies all media files
printInfo(MAX_PLAYBACK_RES_FAILURE_MSG);
return resolutions[RES_1920_1080];
}
int first;
int second;
try {
first = Integer.parseInt(matcher.group(1));
second = Integer.parseInt(matcher.group(2));
} catch (NumberFormatException e) {
// match was found, but not an identifiable resolution
printInfo(MAX_PLAYBACK_RES_FAILURE_MSG);
return resolutions[RES_1920_1080];
}
// ensure that the larger of the two values found is assigned to 'width'
int height = Math.min(first, second);
int width = Math.max(first, second);
return getMaxVideoPlaybackResolution(width, height);
}
/*
* After downloading and unzipping the media files, mLocalMediaPath must be the path to the
* directory containing 'bbb_short' and 'bbb_full' directories, as it is defined in its
* description as an option.
* After extraction, this directory exists one level below the the directory 'mediaFolder'.
* If the 'mediaFolder' contains anything other than exactly one subdirectory, a
* TargetSetupError is thrown. Otherwise, the mLocalMediaPath variable is set to the path of
* this subdirectory.
*/
private void updateLocalMediaPath(File mediaFolder) throws TargetSetupError {
String[] subDirs = mediaFolder.list();
if (subDirs.length != 1) {
throw new TargetSetupError(String.format(
"Unexpected contents in directory %s", mLocalMediaPath));
}
File newMediaFolder = new File(mediaFolder, subDirs[0]);
mLocalMediaPath = newMediaFolder.toString();
}
/*
* Copies the media files to the host from predefined url MEDIA_URL_STRING.
* The compressed file is downloaded and unzipped into mLocalMediaPath.
*/
private void downloadMediaToHost() throws TargetSetupError {
URL url;
try {
url = new URL(MEDIA_URL_STRING);
} catch (MalformedURLException e) {
throw new TargetSetupError(
String.format("Trouble finding android media files at %s", MEDIA_URL_STRING));
}
File mediaFolder = new File(mLocalMediaPath);
File mediaFolderZip = new File(mediaFolder.getAbsolutePath() + ".zip");
try {
mediaFolder.mkdirs();
mediaFolderZip.createNewFile();
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();
BufferedOutputStream out =
new BufferedOutputStream(new FileOutputStream(mediaFolderZip));
byte[] buffer = new byte[1024];
int count;
printInfo("Downloading media files to host");
while ((count = in.read(buffer)) >= 0) {
out.write(buffer, 0, count);
}
out.flush();
out.close();
in.close();
printInfo("Unzipping media files");
ZipUtil.extractZip(new ZipFile(mediaFolderZip), mediaFolder);
} catch (IOException e) {
FileUtil.recursiveDelete(mediaFolder);
FileUtil.recursiveDelete(mediaFolderZip);
throw new TargetSetupError("Failed to open media files on host");
}
}
/**
* Pushes directories containing media files to the device for all directories that:
* - are not already present on the device
* - contain video files of a resolution less than or equal to the device's
* max video playback resolution
*/
protected void copyMediaFiles(ITestDevice device, Dimension mvpr)
throws DeviceNotAvailableException {
int resIndex = RES_176_144;
while (resIndex <= RES_1920_1080) {
Dimension copiedResolution = resolutions[resIndex];
String resString = resolutionString(copiedResolution);
if (copiedResolution.width > mvpr.width || copiedResolution.height > mvpr.height) {
printInfo(String.format(
"Device cannot support resolutions %s and larger, media copying complete",
resString));
return;
}
String deviceShortFilePath = getDeviceShortDir(device, copiedResolution);
String deviceFullFilePath = getDeviceFullDir(device, copiedResolution);
if (!device.doesFileExist(deviceShortFilePath) ||
!device.doesFileExist(deviceFullFilePath)) {
printInfo(String.format("Copying files of resolution %s to device", resString));
String localShortDirName = "bbb_short/" + resString;
String localFullDirName = "bbb_full/" + resString;
File localShortDir = new File(mLocalMediaPath, localShortDirName);
File localFullDir = new File(mLocalMediaPath, localFullDirName);
// push short directory of given resolution, if not present on device
if(!device.doesFileExist(deviceShortFilePath)) {
device.pushDir(localShortDir, deviceShortFilePath);
}
// push full directory of given resolution, if not present on device
if(!device.doesFileExist(deviceFullFilePath)) {
device.pushDir(localFullDir, deviceFullFilePath);
}
}
resIndex++;
}
}
/*
* Returns true if all media files of a resolution less than or equal to 'mvpr' exist on the
* device, and otherwise returns false.
*/
private boolean mediaFilesExistOnDevice(ITestDevice device, Dimension mvpr)
throws DeviceNotAvailableException{
int resIndex = RES_176_144;
while (resIndex <= RES_1920_1080) {
Dimension copiedResolution = resolutions[resIndex];
if (copiedResolution.width > mvpr.width || copiedResolution.height > mvpr.height) {
break; // we don't need to check for resolutions greater than or equal to this
}
String deviceShortFilePath = getDeviceShortDir(device, copiedResolution);
String deviceFullFilePath = getDeviceFullDir(device, copiedResolution);
if (!device.doesFileExist(deviceShortFilePath) ||
!device.doesFileExist(deviceFullFilePath)) {
// media files of valid resolution not found on the device, and must be pushed
return false;
}
resIndex++;
}
return true;
}
/* Static method that returns a directory called 'dirName' in the system's temp directory */
private static File createSimpleTempDir(String dirName) throws IOException {
// find system's temp directory
File throwaway = File.createTempFile(dirName, null);
String systemTempDir = throwaway.getParent();
// create directory with simple name within temp directory
File simpleTempDir = new File(systemTempDir, dirName);
// delete file used to find temp directory
throwaway.delete();
return simpleTempDir;
}
/* Method that creates a local media path, and ensures that the necessary media files live
* within that path */
private void createLocalMediaPath() throws TargetSetupError {
File mediaFolder;
try {
mediaFolder = createSimpleTempDir(MEDIA_FOLDER_NAME);
} catch (IOException e) {
throw new TargetSetupError("Unable to create host temp directory for media files");
}
mLocalMediaPath = mediaFolder.getAbsolutePath();
if (!mediaFolder.exists()) {
// directory has not been created or filled by previous runs of MediaPreparer
downloadMediaToHost(); //download media into mLocalMediaPath
}
updateLocalMediaPath(mediaFolder);
}
/*********************************************************************************************
* PRECONDITION METHODS
*********************************************************************************************/
/**
* Allows installations of apps from sources other than the Play Store
*/
protected void enableThirdPartyInstalls(ITestDevice device)
throws DeviceNotAvailableException {
String shellCmd = String.format("settings put secure %s 1", INSTALL_NON_MARKET_APPS);
device.executeShellCommand(shellCmd);
}
/**
* Prevents the screen from sleeping while charging via USB
*/
protected void enableStayAwakeSetting(ITestDevice device) throws DeviceNotAvailableException {
String shellCmd = String.format("settings put global %s %d",
STAY_ON_WHILE_PLUGGED_IN, BATTERY_PLUGGED_USB);
device.executeShellCommand(shellCmd);
}
/**
* Prevents package verification on apps installed through ADB/ADT/USB
*/
protected void disableAdbAppVerification(ITestDevice device)
throws DeviceNotAvailableException {
String shellCmd = String.format("settings put global %s 0", PACKAGE_VERIFIER_INCLUDE_ADB);
device.executeShellCommand(shellCmd);
}
/**
* Prevents the keyguard from re-emerging during the CTS test, which can cause some failures
* Note: the shell command run here is not supported on L
*/
protected void disableKeyguard(ITestDevice device) throws DeviceNotAvailableException {
device.executeShellCommand("wm disable-keyguard");
}
/**
* Throws a TargetSetupError if location services are not enabled by gps or a network
*/
protected void checkLocationServices(ITestDevice device)
throws DeviceNotAvailableException, TargetSetupError {
String shellCmd = String.format("settings get secure %s", LOCATION_PROVIDERS_ALLOWED);
String locationServices = device.executeShellCommand(shellCmd);
if (!locationServices.contains("gps") && !locationServices.contains("network")) {
// location services are not enabled by gps nor by the network
throw new TargetSetupError(
"Location services must be enabled for several CTS test packages");
}
}
/**
* Prints a warning if the device's locale is something other than US English, as some tests
* may pass or fail depending on the 'en-US' locale.
*/
protected void verifyLocale(ITestDevice device) throws DeviceNotAvailableException {
String locale = device.getProperty(LOCALE_PROPERTY_STRING);
if (!locale.equalsIgnoreCase(US_EN_LOCALE_STRING)) {
printWarning(String.format("Expected locale en-US, detected locale \"%s\"", locale));
}
}
/**
* Prints a warning if the device is not running a user build. This is not allowed for
* testing production devices, but should not block testers from running CTS on a userdebug
* build.
*/
protected void verifyUserBuild(ITestDevice device) throws DeviceNotAvailableException {
String buildType = device.getProperty(BUILD_TYPE_PROPERTY_STRING);
if (!buildType.equalsIgnoreCase(USER_BUILD_STRING)) {
printWarning(String.format("Expected user build, detected type \"%s\"", buildType));
}
}
/**
* Throws a TargetSetupError if the device is not connected to a WiFi network. Testers can
* optionally supply a 'wifi-ssid' and 'wifi-psk' (in the options above) to attempt connection
* to a specific network.
*/
protected void runWifiPrecondition(ITestDevice device)
throws TargetSetupError, DeviceNotAvailableException {
if(!device.connectToWifiNetworkIfNeeded(mWifiSsid, mWifiPsk)) {
throw new TargetSetupError("Unable to find or create network connection, some " +
"modules of CTS require an active network connection");
}
if(mWifiSsid == null) {
// no connection to create, check for existing connectivity
if (!device.checkConnectivity()) {
throw new TargetSetupError("Device has no network connection, no ssid provided");
}
} else {
// network provided in options, attempt to create new connection if needed
if (!device.connectToWifiNetworkIfNeeded(mWifiSsid, mWifiPsk)) {
throw new TargetSetupError("Unable to establish network connection," +
"some CTS packages require an active network connection");
}
}
}
/**
* Checks that media files for the mediastress tests are present on the device, and if not,
* pushes them onto the device.
*/
protected void runMediaPrecondition(ITestDevice device)
throws TargetSetupError, DeviceNotAvailableException {
if (mSkipMediaDownload) {
return; // skip this precondition
}
Dimension mvpr = getMaxVideoPlaybackResolution(device);
if (mediaFilesExistOnDevice(device, mvpr)) {
return; // media files already found on the device
}
if (mLocalMediaPath == null) {
createLocalMediaPath(); // make new path on host containing media files
}
printInfo(String.format("Media files located on host at: %s", mLocalMediaPath));
copyMediaFiles(device, mvpr);
}
public void setUp(ITestDevice device, IBuildInfo buildInfo)
throws TargetSetupError, BuildError, DeviceNotAvailableException {
if (mSkipPreconditions) {
return; // skipping host-side preconditions
}
/* run each host-side precondition */
enableThirdPartyInstalls(device);
enableStayAwakeSetting(device);
disableAdbAppVerification(device);
disableKeyguard(device);
checkLocationServices(device);
verifyLocale(device);
verifyUserBuild(device);
runWifiPrecondition(device);
runMediaPrecondition(device);
}
}