/** ** Copyright (C) SAS Institute, All rights reserved. ** General Public License: http://www.opensource.org/licenses/gpl-license.php **/ package org.safs.android.auto.lib; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.safs.tools.GenericProcessMonitor; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; import com.android.ddmlib.RawImage; /** * Utilities for the Droid testing environment and Android Developer SDK tools. * * @author CANAGL */ public class DUtilities { protected static DUtilities utils = new org.safs.android.auto.lib.DUtilities(); protected void debugI(String message){ System.out.println(message); } /** * Writes to System.out. Subclasses should override to log to a different mechanism. * @param message */ protected static void debug(String message){ utils.debugI(message); } /************************************************************************** * Default: C:\Program Files\Android\android-sdk\<br> * Set to the root directory where the Droid Development SDK is located. **/ public static String ROOT_DROID_SDK_DIR = "C:\\Program Files\\Android\\android-sdk"; /************************************************************************** * Default: C:\Program Files\Android\android-sdk\tools<br> * Set to the directory where the Droid Development SDK Tools are located. **/ public static String ROOT_DROID_SDK_TOOLS = ROOT_DROID_SDK_DIR +File.separator+"tools"; /************************************************************************** * Default: D:\ant182<br> * Set to the root directory where the ant Development SDK is located. **/ public static String ROOT_ANT_SDK_DIR = "D:\\ant182"; /************************************************************************** * Default: "C:\\SAFS\samples\Droid" * Set to the root directory where the Droid project files are located. **/ public static String ROOT_DROID_PROJECT_DIR = "C:\\SAFS\\samples\\Droid"; /************************************************************************** * Default: SAFSMessenger\bin\SAFSTCPMessenger-debug.apk<br> * Set to the path to the SAFS Communication Service app. **/ public static String SAFS_SERVICE_APP = "SAFSTCPMessenger\\bin\\SAFSTCPMessenger-debug.apk"; /************************************************************************** * Default: org.safs.android.messenger<br> * Set to the expected package name of the SAFS Service. **/ public static String SAFS_SERVICE_PACKAGE = "org.safs.android.messenger"; /************************************************************************** * Default: ? RobotiumTestRunner\bin\RobotiumTestRunner-debug.apk ?<br> * Set to the path to the Test Runner app. **/ public static String TEST_RUNNER_APP = "RobotiumTestRunner\\bin\\RobotiumTestRunner-debug.apk"; /************************************************************************** * Default: RobotiumTestRunner<br> * Set to the path to the Test Runner app's source folder. **/ public static String TEST_RUNNER_APP_SOURCE = "RobotiumTestRunner"; /************************************************************************** * Default: com.jayway.android.robotium.remotecontrol.client<br> * Set to the expected package name of the Test Runner. **/ public static String TEST_RUNNER_PACKAGE = "com.jayway.android.robotium.remotecontrol.client"; /************************************************************************** * Default: com.jayway.android.robotium.remotecontrol.client/com.jayway.android.robotium.remotecontrol.client.RobotiumTestRunner<br> * Set to the expected package and class name of the Test Runner Instrument. **/ public static String TEST_RUNNER_INSTRUMENT = "com.jayway.android.robotium.remotecontrol.client/com.jayway.android.robotium.remotecontrol.client.RobotiumTestRunner"; /************************************************************************** * Default: pathTo\bin\ApiDemos-debug.apk<br> * Set to the path to the target Application. **/ public static String TEST_TARGET_APP = "SAFSAPIDemo\\bin\\ApiDemos-debug.apk"; /************************************************************************** * Default: org.safs.android.engine<br> * Set to the expected package name of the target Application. **/ public static String TEST_TARGET_PACKAGE = "com.android.samples.apidemos"; /************************************************************************** * If {@link #TEST_TARGET_PACKAGE} is set, this field will be true. **/ public static boolean IS_TEST_TARGET_PACKAGE_SET = false; /** * The parameters used by 'adb' to install an application on device/emulator<br> * You should modify the third value in this array<br> * Attention: use the clone of this field as it is static and shared by threads<br> * <pre> * Usage: * String[] params = installParams.clone(); * params[2] = "yourApp.apk"; * </pre> * * @see #SAFS_SERVICE_APP * @see #SAFS_ENGINE_APP * @see #TEST_TARGET_APP * */ static String[] installParams = {"install", "-r", "application.apk"}; /** * The parameters used by 'adb' to uninstall an application on device/emulator<br> * This version keeps application data and cache.<br> * You should modify the last value in this array<br> * Attention: use the clone of this field as it is static and shared by threads<br> * <pre> * Usage: * String[] params = uninstallParams.clone(); * params[2] = "your.app.package"; * </pre> * * @see #SAFS_SERVICE_PACKAGE * @see #TEST_RUNNER_PACKAGE * @see #TEST_TARGET_PACKAGE */ static String[] uninstallParamsKeepData = {"uninstall", "-k", "application.package"}; /** * The parameters used by 'adb' to uninstall an application on device/emulator<br> * This version also clears application data and cache.<br> * You should modify the last value in this array<br> * Attention: use the clone of this field as it is static and shared by threads<br> * <pre> * Usage: * String[] params = uninstallParams.clone(); * params[1] = "your.app.package"; * </pre> * * @see #SAFS_SERVICE_PACKAGE * @see #TEST_RUNNER_PACKAGE * @see #TEST_TARGET_PACKAGE */ static String[] uninstallParams = {"uninstall", "application.package"}; /** * The parameters used by 'adb' to launch a test runner on device/emulator<br> * You should modify the fourth value in this array<br> * Attention: use the clone of this field as it is static and shared by threads<br> * <pre> * Usage: * String[] params = launchTestCaseParams.clone(); * params[3] = "yourRunnerInstrument"; * </pre> * * @see #SAFS_ENGINE_INSTRUMENT * @see #ROBOTIUM_ENGINE_INSTRUMENT */ static String[] launchTestCaseParams = {"shell", "am", "instrument", "com.jayway.android.robotium.remotecontrol.client/com.jayway.android.robotium.remotecontrol.client.RobotiumTestRunner"}; public static final String MANIFEST_XML_FILENAEM = "AndroidManifest.xml"; /************************************************************************** * Empty until set. * Set to the (optional) DeviceSerial stored in the INI config file. **/ public static String DEFAULT_DEVICE_SERIAL = ""; /************************************************************************** * Empty until set. * Set to the value to be used by the Android Debug Bridge (adb). * This is normally empty, but may get set during device interrogation. * When empty, it means use the one (and only) detected device/emulator. **/ public static String USE_DEVICE_SERIAL = ""; /************************************************************************** * null until set. * Set to the name of the default EMULATOR AVD, if any. **/ public static String DEFAULT_EMULATOR_AVD = null; public static final String DEVICE_STRING = "device"; public static final String OFFLINE_STRING = "offline"; /************************************************************************** * 180 (seconds) by default. * Set to the default emulator/device launch timeout. **/ public static int REMOTE_DROID_LAUNCH_TIMEOUT = 180; /** 20. Default timeout in seconds to wait for remote log commencement. */ public static int REMOTE_LAUNCH_TIMEOUT = 20; /** static android sdk tool to the one appropriate for the OS (Windows or Unix). */ protected static AndroidTools androidsdk = null; /** static ant sdk tool to the one appropriate for the OS (Windows or Unix). */ protected static AntTool anttool = null; /** * Default TRUE flag to (re)install the AUT Target Package APK upon launch. */ public static boolean installAUT = true; /** * Default FALSE flag to (re)install the SAFS TCP Messenger Package APK upon launch. * Normally you don't have to reinstall this since it is the same for all tested apps. */ public static boolean installMessenger = false; /** * Default TRUE flag to (re)install the Test Runner APK upon launch. */ public static boolean installRunner = true; /** * Default FALSE flag to rebuild the Test Runner APK before installing it at launch time. * Typically this is done if the Test Runner APK needs to be modified to accommodate a new * AUT Target Package. Not too often. */ public static boolean rebuildRunner = false; /** * Default NULL value of arguments to pass to the Ant launcher used to rebuild the * Test Runner APK before installing it at launch time. * <p> * On Windows, a very common Ant launcher arg to pass is "-noclasspath" to force the Ant build to * ignore the Windows CLASSPATH Environment variable. * <p> * This value should be considered separate from the typical "debug" or "release" args * sent to Ant. Instead, these are for the platform-specific Ant launchers: ant.bat, etc.. **/ public static String[] rebuildRunnerAntArgs = null; /** * Default FALSE flag. Do not rebuild the runner if we think nothing has changed in the * AndroidManifest.XML. Change this to True to force a rebuild that otherwise would not * proceed due to no detected changes in the automated processing of the AndroidManifest. * <p> * Note: rebuildRunner must also be true. This force override only applies if a rebuild * has been requested, but the detection of unchanged AndroidManifest XML would otherwise * forego the actual rebuild. */ public static boolean rebuildRunnerForce = false; /** java.lang.SecurityException: Permission Denial */ /** android.util.AndroidException: INSTRUMENTATION_FAILED:*/ private final static Pattern ANDROID_EXCEPTION = Pattern.compile(".*Exception:.*"); private final static Pattern INSTRUMENTATION_FAILED = Pattern.compile(".*INSTRUMENTATION_FAILED.*"); /** * This field contains the resign jar file's absolute name, for example, "C:\safs\lib\re-sign.jar". * This jar is used to resign the AUT {@link #TEST_TARGET_APP} automatically. If this field is * null, the AUT {@link #TEST_TARGET_APP} will not be resigned. * <p> * This field only take effect when field {@link #installAUT} is true. * <p> */ public static String RESIGN_JAR_FULL_NAME = null; /** * <em>Note:</em> Risk!!! If some threads modify {@link #TEST_TARGET_APP}, {@link #TEST_RUNNER_APP} * or {@link #SAFS_SERVICE_APP} during this method is called, wrong. use synchronized block? * for each APK whose install boolean is still true, install the apk.<br> * Will attempt to install the AUT APK, the TestRunner APK, and the TCP Messenger APK<br> * * @return false if any installable APK did not successfully install. * Otherwise returns true. */ public static boolean installEnabledAPKs(){ if(installAUT){ debug("DUtilities flagged to install "+ TEST_TARGET_APP +"..."); try{ installReplaceAPK(TEST_TARGET_APP);} catch(Exception x){ debug("INSTALL AUT "+ x.getClass().getSimpleName()+" "+ x.getMessage()); return false; } }else{ debug("DUtilities is NOT flagged to install "+ TEST_TARGET_APP +"..."); } if(installMessenger){ debug("DUtilities flagged to install "+ SAFS_SERVICE_APP +"..."); try{ installReplaceAPK(SAFS_SERVICE_APP);} catch(Exception x){ debug("INSTALL MESSENGER "+ x.getClass().getSimpleName()+" "+ x.getMessage()); return false; } }else{ debug("DUtilities is NOT flagged to install "+ SAFS_SERVICE_APP +"..."); } if(installRunner){ debug("DUtilities flagged to install "+ TEST_RUNNER_APP +"..."); try{ installReplaceAPK(TEST_RUNNER_APP);} catch(Exception x){ debug("INSTALL RUNNER "+ x.getClass().getSimpleName()+" "+ x.getMessage()); return false; } }else{ debug("DUtilities is NOT flagged to install "+ TEST_RUNNER_APP +"..."); } return true; } /** * <em>Note:</em> Risk!!! If some threads modify {@link #TEST_TARGET_APP}, {@link #TEST_RUNNER_APP} * or {@link #TEST_RUNNER_APP_SOURCE} during this method is called, wrong. use synchronized block? * Try to rebuild the test-runner apk.<br> * This will modify the filed {@link #testRunnerApk}<br> * You should call this method before calling {@link #installEnabledAPKs()}<br> * * @return boolean, true if the rebuild succeed; false otherwise. * @see #installEnabledAPKs() */ public static boolean rebuildRunner(){ if(rebuildRunner){ debug("DUtilities attempting to rebuild "+ TEST_RUNNER_APP +"..."); try{ TEST_RUNNER_APP = rebuildTestRunnerApk(TEST_RUNNER_APP_SOURCE, TEST_TARGET_APP, TEST_RUNNER_APP,TEST_RUNNER_INSTRUMENT, true); if(TEST_RUNNER_APP==null) { debug("DUtilities unexpected failure to rebuild "+ TEST_RUNNER_APP); return false; } }catch(Exception x){ debug("DUtilities "+ x.getClass().getSimpleName()+" failure to rebuild "+ TEST_RUNNER_APP); return false; } debug("DUtilities successful rebuilding "+ TEST_RUNNER_APP); }else { debug("DUtilities rebuild not indicated for "+ TEST_RUNNER_APP ); } return true; } /** * <em>Note:</em> Risk!!! If some threads modify {@link #TEST_RUNNER_INSTRUMENT} * during this method is called, wrong. use synchronized block? */ public static boolean launchTestInstrumentation(){ return launchTestInstrumentation(TEST_RUNNER_INSTRUMENT); } /** * Set our static android sdk tool to the one appropriate for the OS (Windows or Unix).<br> * The routine does nothing if the appropriate sdk instance is already set.<br> * * For the sdk's tool home, it firstly try to set it to parameter 'androidToolHome'<br> * If androidToolHome is not a valid home path:<br> * it will try to get it from the 'VM properties' {@link AndroidTools#ANDROID_HOME_SYS_PROP}<br> * or from 'systeme environment' {@link AndroidTools#ANDROID_HOME_ENV_VAR}<br> * or from 'systeme environment' {@link AndroidTools#ANDROID_SDK_ENV_VAR}<br> * * @param androidToolHome String, the android tool's sdk home path * @see org.safs.android.auto.lib.AndroidTools */ public static AndroidTools getAndroidTools(String androidToolHome){ if (androidsdk == null){ debug("Attempting to initialize Android Tools..."); androidsdk = AndroidTools.get(); String toolHome = androidToolHome; try{ androidsdk.setToolHome(toolHome); }catch(IllegalStateException ise){ debug(ise.getMessage()); toolHome = androidsdk.getToolHome(); } debug("Setting Android Tools SDK Dir to "+ toolHome); } return androidsdk; } /** * Writes to System.err. Subclasses should override to log to a different mechanism. * @param message */ protected static void error(String message){ System.err.println(message); } protected static GenericProcessMonitor mon = null; /** * Returns a blank instance of GenericProcessMonitor on which to call the necessary * static methods. Subclasses should override to get an appropriate subclass of * GenericProcessMonitor. */ protected static GenericProcessMonitor getProcessMonitor(){ return mon == null ? new GenericProcessMonitor(): mon; } /** * You can always change the {@link #androidsdk}'s home path by calling this method.<br> * But be careful, if you call this method, you will modify the tool-home of Singleton {@link AndroidTools},<br> * and the other thread may be affected when the use AndroidTools.<br> * * @param androidToolHome String, the android tool's sdk home path */ public static void setAndroidToolsHome(String androidToolHome){ if(androidsdk==null){ getAndroidTools(androidToolHome); }else{ try{ androidsdk.setToolHome(androidToolHome); }catch(IllegalStateException ise){ debug(ise.getMessage()); } } } /** * Before calling this method:<br> * Remember to set {@link #ROOT_DROID_SDK_DIR}<br> * or set 'vm properties' {@link AndroidTools#ANDROID_HOME_SYS_PROP}<br> * or set 'systeme environment' {@link AndroidTools#ANDROID_HOME_ENV_VAR}<br> * or set 'systeme environment' {@link AndroidTools#ANDROID_SDK_ENV_VAR}<br> * */ protected static void initAndroidTools(){ getAndroidTools(ROOT_DROID_SDK_DIR); } /** * Set our static ant sdk tool to the one appropriate for the OS (Windows or Unix).<br> * The routine does nothing if the appropriate sdk instance is already set.<br> * * For the sdk's tool home, it firstly try to set it to parameter 'antToolHome'<br> * If antToolHome is not a valid home path:<br> * it will try to get it from the 'VM properties' {@link AntTool#ANT_HOME_PROP}<br> * or from 'systeme environment' {@link AntTool#ANT_HOME_ENV}<br> * * @param antToolHome String, the ant tool's sdk home path * @see org.safs.android.auto.lib.AntTool */ public static AntTool getAntTool(String antToolHome){ if (anttool == null){ debug("Attempting to initialize Ant Tool ..."); anttool = AntTool.instance(); String toolHome = antToolHome; try{ anttool.setToolHome(toolHome); }catch(IllegalStateException ise){ debug(ise.getMessage()); toolHome = anttool.getToolHome(); } debug("Setting Ant Tool SDK Dir to "+ toolHome); } return anttool; } /** * You can always change the {@link #anttool}'s home path by calling this method.<br> * But be careful, if you call this method, you will modify the tool-home of Singleton {@link AntTool},<br> * and the other thread may be affected when the use AntTool.<br> * * @param antToolHome String, the ant tool's sdk home path */ public static void setAntToolsHome(String antToolHome){ if(anttool==null){ getAntTool(antToolHome); }else{ try{ anttool.setToolHome(antToolHome); }catch(IllegalStateException ise){ debug(ise.getMessage()); } } } /** * Before calling this method:<br> * Remember to set {@link #ROOT_ANT_SDK_DIR}<br> * or set 'vm properties' {@link AntTool#ANT_HOME_PROP}<br> * or set 'systeme environment' {@link AntTool#ANT_HOME_ENV}<br> * */ protected static void initAntTool(){ getAntTool(ROOT_ANT_SDK_DIR); } /** * Extract the list of Android Debug Bridge attached devices from Android Tools * <p> * adb devices * * @return String[] of 0 or more devices--which may be "device" or "offline". * @throws RuntimeException if there is a problem executing the Android Debug Bridge (adb) */ public static List<String> getAttachedDevices() throws RuntimeException{ Process2 process = null; ArrayList<String> rs = new ArrayList<String>(); BufferedReader reader = null; try{ if(androidsdk==null) initAndroidTools(); process = androidsdk.adb("devices"); boolean finished = false; reader = process.getStdoutReader(); String line = null; while(!finished){ try{ process.exitValue(); finished = true; } catch(IllegalThreadStateException x){ while(reader.ready()){ line = reader.readLine(); if(line != null){ if(line.trim().endsWith(DEVICE_STRING)) rs.add(line); if(line.trim().endsWith(OFFLINE_STRING)) rs.add(line); } } } } while(reader.ready()){ line = reader.readLine(); if(line != null){ if(line.trim().endsWith(DEVICE_STRING)) rs.add(line); if(line.trim().endsWith(OFFLINE_STRING)) rs.add(line); } } try{ reader.close();}catch(Exception x){} }catch(IOException x){ debug("Error finding/running adb command: "+ x.getClass().getSimpleName()+", "+x.getMessage()); if(reader!=null) try{ reader.close(); reader = null;}catch(Exception x2){} if(process!=null) try{process.destroy();process = null;}catch(Exception x2){} throw new RuntimeException("adb runtime error: "+ x.getClass().getSimpleName()+", "+x.getMessage()); } if(reader!=null) try{ reader.close();reader = null;}catch(Exception x2){} if(process!=null) try{process.destroy();process = null;}catch(Exception x2){} return rs; } /** * ADB: The central point to communicate with any devices, emulators, or the applications running on them. */ public static AndroidDebugBridge bridge = null; /** * Get the AndroidDebugBridge instance. * @return AndroidDebugBridge */ public static AndroidDebugBridge getAndroidDebugBridge(){ String debugmsg = "DU.getAndroidDebugBridge(): "; try { if(bridge==null){ bridge = AndroidDebugBridge.getBridge(); if(bridge==null){ // init the lib String adbLocation = System.getProperty("com.android.screenshot.bindir"); if (adbLocation != null && adbLocation.length() != 0) { adbLocation += File.separator + "adb"; } else { adbLocation = "adb"; } AndroidDebugBridge.init(false /* debugger support */); bridge = AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */); } } }catch(Exception e){ debug(debugmsg+"Met "+e.getClass().getSimpleName()+":"+e.getMessage()); } return bridge; } /** * Return the device that we want to test with. * If there is only one device, we will return it.<br> * If there are multiple devices, we will try to return the device defined by DUtilities.DEFAULT_DEVICE_SERIAL<br> * If DUtilities.DEFAULT_DEVICE_SERIAL is not avaiable or we can't find a matched device, then return the first oen.<br> * * @return IDevice */ public static IDevice getIDevice() { IDevice target = null; String debugmsg = "DU.getIDevice(): "; try { bridge = getAndroidDebugBridge(); if(bridge==null){ debug(debugmsg+"Error: Can't get the Android Debug Bridge!!!"); }else{ // we can't just ask for the device list right away, as the internal thread getting // them from ADB may not be done getting the first list. // Since we don't really want getDevices() to be blocking, we wait here with a timeout 10 seconds. int count = 0; while (bridge.hasInitialDeviceList() == false) { try { Thread.sleep(100); count++; } catch (InterruptedException e) { debug(debugmsg+"Ignore Exception "+e.getClass().getSimpleName()+":"+e.getMessage()); } if (count > 100) { debug(debugmsg+"Timeout reached, can't get device list!"); return null; } } IDevice[] devices = bridge.getDevices(); if (devices.length == 0) { debug(debugmsg+"No devices found!"); }else if(devices.length == 1){ target = devices[0]; }else{ //If there are more than one device attached String serialNumber = null; if(DUtilities.DEFAULT_DEVICE_SERIAL.length()>0){ //If we have a default one defined by DUtilities.DEFAULT_DEVICE_SERIAL, we try to get it. for(IDevice d: devices){ serialNumber = d.getSerialNumber(); debug(debugmsg+"Attempting match device '"+ serialNumber +"' with default '"+ DUtilities.DEFAULT_DEVICE_SERIAL +"'"); if(serialNumber!=null && serialNumber.toLowerCase().startsWith(DUtilities.DEFAULT_DEVICE_SERIAL.toLowerCase())){ target = d; break; } } if(target==null){ debug(debugmsg+"We didn't find a device matching with the default one. Return the first one."); target = devices[0]; } }else{ //If we don't have a default one (DUtilities.DEFAULT_DEVICE_SERIAL is empty), get the first device target = devices[0]; } } if (target != null) { debug(debugmsg+"Got device: " + target.getSerialNumber()); } else { debug(debugmsg+"Could not find matching device/emulator."); } } }catch(Exception e){ debug(debugmsg+"Met Exception "+e.getClass().getSimpleName()+":"+e.getMessage()); } return target; } /** * Grab an image from an ADB-connected device. * @param device, IDevice: the android device or emulator, it can be got by {@link #getIDevice()} * @param rotation, int: the device rotation in degree <br> * we should rotate the image with inverse degree (360-rotation)<br> * Only rotatable is true, rotation will take effects. * @param rotatable, boolean: if the application is rotatable * * @see #getIDevice() */ public static BufferedImage getDeviceScreenImage(IDevice device, int rotation, boolean rotatable, String fileName, String filePath){ RawImage rawImage = null; BufferedImage image = null; String debugmsg = "DUtilities.getDeviceScreenImage(): "; try { if(device.isEmulator()){ debug(debugmsg+"try to get screen image for emulator "+device.getAvdName()); }else{ debug(debugmsg+"try to get screen image for device "+device.getSerialNumber()); } rawImage = device.getScreenshot(); // device/adb not available? if (rawImage != null){ image = ImageUtils.convertImage(rawImage); if(rotatable){ //When rotate the image got from ImageUtils.convertImage(), sometimes an ImagingOpException //will be thrown out. ImagingOpException: Unable to transform src image //So get a copy of the original image and rotate it. image = ImageUtils.getCopiedImage(image, image.getWidth(), image.getHeight(), null); //According to the rotation, rotate the image back. image = ImageUtils.rotateImage(image, (360-rotation)%360); } }else{ debug(debugmsg+"Can't get raw image from the device."); } }catch (Exception e) { debug(debugmsg+"Met " + e.getClass().getSimpleName()+":"+e.getMessage()); } return image; } /** * Attempt to launch a prestored Emulator -avd and a -no-snapstorage argument. * <p> * emulator -avd (avd) * * @return boolean true if emulator launch detects a new adb "device" within REMOTE_DROID_LAUNCH_TIMEOUT. * false if a new device is not detected in that period. * @throws AndroidRuntimeException if an error occurs while trying to launch the emulator or * trying to extract the number of adb devices. */ public static boolean launchEmulatorAVD(String avd)throws AndroidRuntimeException{ try{ if(androidsdk==null)initAndroidTools(); StartEmulator em = new StartEmulator(); em.setDoReaperThread(false); em.setDoSocketThread(false); em.setDoOpenSocket(false); em.setDoCloseSocket(false); em.setChainedStdOut(System.out); // this call will block until the emulator boot process is complete em.run(new String[]{"-no-snapstorage", "-avd", avd}); debug("Emulator launch appears successful."); em.getEmulatorProcess().destroy(); return true; }catch(AndroidRuntimeException x){ debug("Error launching emulator : "+ x.getClass().getSimpleName()+", "+ x.getMessage()); return false; }catch(Exception x){ debug("Error launching/running emulator : "+ x.getClass().getSimpleName()+", "+ x.getMessage()); throw new AndroidRuntimeException("emulator runtime error: "+ x.getMessage()); } } /** * Attempt to shutdown a specific emulator Process. * @param emulator -- known valid values: "emulator", "emulator-arm", "emulator-mips", "emulator-x86" * @return true if we saw the Process and (hopefully) shut down the Process. */ public static boolean shutdownEmulatorProcess(String emulator) throws IOException{ boolean shutdown = false; GenericProcessMonitor monitor = getProcessMonitor(); shutdown = monitor.shutdownProcess(emulator); if(!shutdown) { emulator += ".exe"; shutdown = monitor.shutdownProcess(emulator); } return shutdown; } /** * Attempts to shutdown (process destroy!) any emulators we have launched. * This is NOT a graceful shutdown, but an attempt to destroy any emulator processes * we have launched with the StartEmulator tool. This should be OK for emulators * we normally launch with the -no-snapstorage option. * <p> * Note the built-in process to signal an emulator shutdown does not seem to work on * Windows. Windows runs emulator.exe which launches emulator-arm.exe. The shutdown * request does not seem to impact emulator-arm.exe. * <p> * @param shutdownAnyEmulator to attempt to kill any emulator, whether we started it or not. * @return true if we detect at least one running emulator we have launched has responded * with a positive shutdown attempt. We will look for this signal for no more than 5 seconds. */ public static boolean shutdownLaunchedEmulators(boolean shutdownAnyEmulator){ String match = "true"; System.setProperty(StartEmulator.EMULATOR_DESTROY_PROPERTY, match); int timeout = 0; boolean destroyed = false; while (timeout++ < 11 && !destroyed){ try{destroyed = match.equals(System.getProperty(StartEmulator.EMULATOR_DESTROYED_PROPERTY));} catch(Exception x){} if(!destroyed) try{Thread.sleep(500);}catch(Exception x){} } if(destroyed){ debug("StartEmulator reports success on receipt of shutdown request."); } if(shutdownAnyEmulator){ debug("DUtilities attempting ProcessMonitor shutdownProcess..."); boolean success = false; try{ success = shutdownEmulatorProcess("emulator"); if(!success) success = shutdownEmulatorProcess("emulator-arm"); if(!success) success = shutdownEmulatorProcess("emulator-mips"); if(!success) success = shutdownEmulatorProcess("emulator-x86"); }catch(Exception x){ debug("Emulator shutdown "+ x.getClass().getSimpleName() +": "+ x.getMessage()); } destroyed = success; } debug("Emulator(s) shutdown? "+ destroyed); return destroyed; } /** * Calls getAttachedDevices and returns true if any are offline. * @return true if getAttchedDevices returns any device as "offline" * @see #getAttachedDevices() */ public static boolean isDeviceOffline(){ boolean result = false; List<String> devices = null; debug("Checking for devices going offline..."); try{ devices = getAttachedDevices();} catch(Exception x){} String device = null; if(devices.size()> 0){ debug("Checking "+ devices.size() + " for 'offline' status...."); for(int i=0;i<devices.size();i++){ device = (String)devices.get(i); if(device.trim().endsWith(OFFLINE_STRING)){ debug("detected OFFLINE device: "+ device); result = true; } } } if(!result) debug("No 'offline' devices detected."); return result; } /** * Execute adb kill-server then adb start-server back-to-back with a 4 second delay between. */ public static void resetADBServer(){ if(androidsdk==null) initAndroidTools(); Process2 process = null; debug("Resetting ADB Server..."); try{ process = androidsdk.adb("kill-server"); process.waitFor().destroy(); }catch(Exception x){} try{Thread.sleep(4000);}catch(Exception x){} debug("Starting ADB Server..."); try{ process = androidsdk.adb("start-server"); process.waitFor().destroy(); }catch(Exception x){} } /** * Attempt an adb "start-server" command. */ public static void startADBServer(){ if(androidsdk==null) initAndroidTools(); Process2 process = null; debug("adb start-server commencing..."); try{ process = androidsdk.adb("start-server"); process.forwardOutput().waitFor(); }catch(Exception x){ debug("adb start-server "+x.getClass().getSimpleName()+", "+x.getMessage()); } if(process != null) try{ process.destroy();}catch(Exception x){} process = null; } /** * Attempt an adb "kill-server" command. */ public static void killADBServer(){ if(androidsdk==null) initAndroidTools(); Process2 process = null; debug("adb kill-server commencing..."); try{ process = androidsdk.adb("kill-server"); process.forwardOutput().waitFor(); }catch(Exception x){ debug("adb kill-server "+x.getClass().getSimpleName()+", "+x.getMessage()); } if(process != null) try{ process.destroy();}catch(Exception x){} process = null; } /** * Install the single APK provided in apkPath with the -r (replace) option. * Currently the routine provides for a 20 second timeout for the install. * <p> * * @param apkPath to APK ready for installation. * @throws RuntimeException if the process was interrupted, had an IOException, or * did not exit in the timeout period or did not exit with success. */ public static void installReplaceAPK(String apkPath)throws RuntimeException{ debug("INSTALLING "+apkPath); if(androidsdk==null) initAndroidTools(); if(!waitDevice()){ throw new RuntimeException(("OFFLINE device failure... during installing '"+apkPath+"'")); } String[] params = installParams.clone(); params[2] = apkPath; Process2 proc = null; try{ params = addDeviceSerialParam(params); // debug("ATTEMPTING ADB Install command: adb "+ StringUtils.arrayToString(params, " ")); debug("ATTEMPTING ADB Install command: adb "+ Arrays.toString(params)); proc = androidsdk.adb(addDeviceSerialParam(params)); proc.forwardOutput().waitForSuccess(60).destroy(); debug("ADB Install command successful."); proc = null; } catch(Throwable x){ String msg = "Failed to install '"+ apkPath+ "' due to "+ x.getClass().getSimpleName()+", "+ x.getMessage(); debug(msg); if(proc != null) { try{ proc.destroy(); proc = null; }catch(Exception x2){proc = null;} } throw new RuntimeException(msg); } } /** * Uninstall (remove) the single APK (package) provided in apkPackage. * <p> * * @param apkPackage the name of the APK package to uninstall * @param keepData set true to keep the app data and cache along with the uninstall. * @throws RuntimeException if the process was interrupted, had an IOException, or * did not exit with success. */ public static void uninstallAPKPackage(final String apkPackage, final boolean keepData)throws RuntimeException{ debug("UNINSTALLING "+apkPackage +", KEEPDATA="+String.valueOf(keepData)); if(androidsdk==null) initAndroidTools(); if(!waitDevice()){ throw new RuntimeException(("OFFLINE device failure... during installing '"+apkPackage+"'")); } String[] params = keepData ? uninstallParamsKeepData.clone():uninstallParams.clone(); int field = params.length - 1; params[field] = apkPackage; params = addDeviceSerialParam(params); Process2 proc = null; try{ debug("ATTEMPTING ADB uninstall command: adb "+ Arrays.toString(params)); proc = androidsdk.adb(params); proc.forwardOutput().waitForSuccess().destroy(); proc = null; } catch(Exception x){ String msg = "Failed to uninstall '"+ apkPackage+ "' due to "+ x.getClass().getSimpleName()+", "+ x.getMessage(); debug(msg); if(proc != null) { try{ proc.destroy(); proc = null; }catch(Exception x2){proc = null;} } throw new RuntimeException(msg); } } /** * Launches the test instrumentation which should also automatically launch our remote TCP Messenger. * There is normally a 5 second Thread sleep after a successful launch to let the channels get * synchronized. * @return true if we successfully launched the test instrumentation. */ public static boolean launchTestInstrumentation(String instrumentArg) throws RuntimeException{ debug("LAUNCHING "+instrumentArg); boolean instrumentLaunched = true; if(androidsdk==null) initAndroidTools(); if(!waitDevice()){ debug(("OFFLINE device failure... during launch '"+instrumentArg+"'")); instrumentLaunched = false; }else{ String[] params = launchTestCaseParams.clone(); params[3] = instrumentArg; Process2 proc = null; BufferedReader stdout = null; BufferedReader stderr = null; try{ proc = androidsdk.adb(addDeviceSerialParam(params)); //the process to execute "adb shell am instrument xxx" will not fail, but just print message to stdout //so we have to analyse the output to see if there are some problem //android.util.AndroidException: INSTRUMENTATION_FAILED: org.safs.android.engine/org.safs.android.engine.DSAFSTestRunner //java.lang.SecurityException: Permission Denial String tempstr = null; //stdout stdout = proc.getStdoutReader(); while((tempstr=stdout.readLine())!=null){ debug(tempstr); if(!instrumentLaunched) continue; if(ANDROID_EXCEPTION.matcher(tempstr).matches() || INSTRUMENTATION_FAILED.matcher(tempstr).matches()){ instrumentLaunched = false; } } //stderr stderr = proc.getStderrReader(); while((tempstr=stderr.readLine())!=null){ error(tempstr); if(instrumentLaunched) instrumentLaunched = false; } proc.waitForSuccess(); proc = null; } catch(Exception x){ debug("May have failed to launch test instrument '"+ instrumentArg+ "' due to "+ x.getClass().getSimpleName()+", "+ x.getMessage()); instrumentLaunched = false; }finally{ if(proc != null) try{proc.destroy();}catch(Exception x2){} if(stdout != null) try{stdout.close();}catch(Exception x2){} if(stderr != null) try{stderr.close();}catch(Exception x2){} } try{Thread.sleep(5000);}catch(Exception x){} } return instrumentLaunched; } /** * Send an appropriate adb command to unlock the screen. * The method will use {@link #addDeviceSerialParam(String[])} to the default command. * @return true upon success, false otherwise. */ public static boolean unlockDeviceScreen(){ String[] params = new String[]{"shell", "input", "keyevent", "82"}; if(androidsdk==null) initAndroidTools(); Process2 proc = null; try{ proc = androidsdk.adb(addDeviceSerialParam(params)); proc.forwardOutput().waitForSuccess().destroy(); proc = null; } catch(Exception x){ debug("May have failed to unlock device screen due to "+ x.getClass().getSimpleName()+", "+ x.getMessage()); if(proc != null) try{proc.destroy();}catch(Exception x2){} return false; } return true; } /** * Add the petential parameter "-s serivalNumber" to the parameters passed to command 'adb'.<br> * This petential parameter is stored in {@link #USE_DEVICE_SERIAL}<br> * * @param params String[], string array used by command 'adb' * @return String[], string array used by command 'adb' * @see #USE_DEVICE_SERIAL */ public static String[] addDeviceSerialParam(String[] params){ if(params==null){ debug("Array params is null."); return params; } if("".equals(USE_DEVICE_SERIAL)) return params; else{ String[] serialParams = USE_DEVICE_SERIAL.trim().split(" "); String[] newParams = new String[params.length+serialParams.length]; for(int i=0;i<serialParams.length;i++){ newParams[i] = serialParams[i]; } for(int i=0;i<params.length;i++){ newParams[i+serialParams.length] = params[i]; } return newParams; } } /** * The routine will attempt to make sure adb devices are valid and not offline * @return */ public static boolean waitDevice(){ int count = 0; boolean deviceIsOffline = isDeviceOffline(); while(deviceIsOffline && count++ < 2){ try{Thread.sleep(4000);}catch(Exception x){} deviceIsOffline = isDeviceOffline(); } if(deviceIsOffline) { debug("OFFLINE device failure.."); } return !deviceIsOffline; } /** * Get the value of attribute 'package' from the tag <manifest> of AndroidManifest.xml of apk.<br> * * @param apkFile String, the apk file from which we want to get the "package's value" * @return the "package's value", or null if not found */ public static String getTargetPackageValue(String apkFile){ String targetPackage = null; String errmsg = ""; //aapt d xmltree RobotiumTestRunner-debug.apk AndroidManifest.xml | findstr package= // String[] findAutPackageName = {"d", "xmltree", "", "AndroidManifest.xml", "|", "findstr", "package=" }; String[] findAutPackageName = {"d", "xmltree", "", MANIFEST_XML_FILENAEM }; findAutPackageName[2] = apkFile; String packagePrefix = "A: package=\""; String packageSuffix = "\""; int index = -1; BufferedReader in = null; BufferedReader err = null; boolean packageAttributFound = false; Process2 process = null; try { if(androidsdk==null) initAndroidTools(); debug("aapt "+ argsToString(findAutPackageName)); process = androidsdk.aapt(findAutPackageName); String tmpmsg = null; //Try to get the aut's package value from process's stdout in = process.getStdoutReader(); while ((tmpmsg = in.readLine()) != null) { if(packageAttributFound) continue; index = tmpmsg.indexOf(packagePrefix); if(index>-1){ targetPackage = tmpmsg; packageAttributFound = true; } } if(packageAttributFound){ debug("raw aut's package is '" + targetPackage + "'"); index = targetPackage.indexOf(packagePrefix); if (index>-1){ targetPackage = targetPackage.substring(index + packagePrefix.length()); index = targetPackage.indexOf(packageSuffix); if (index >-1){ targetPackage = targetPackage.substring(0, index); } } debug("aut's package is '" + targetPackage + "'"); } if(targetPackage==null || targetPackage.trim().equals("")){ debug("can't get aut's package value."); packageAttributFound = false; } //Try to get any error message err = process.getStderrReader(); while ((tmpmsg = err.readLine()) != null) { errmsg += tmpmsg; } try { process.waitForSuccess(); } catch (InterruptedException e) { debug("During get aut's package, met Exception="+e.getMessage()); } catch (RuntimeException re){ debug("During get aut's package, met Exception="+re.getMessage()); } if(!errmsg.equals("")){ debug("Process Error Message: "+errmsg); } } catch (Throwable e) { debug("During get aut's package, met Exception="+e.getMessage()); } finally{ try { if(in!=null) in.close(); } catch (IOException e) {} try { if(err!=null) err.close(); } catch (IOException e) {} } try{ process.destroy(); process = null;}catch(Exception x){} if(!packageAttributFound){ debug("AUT's package value NOT found!"); }else{ debug("GOT aut's package: '" + targetPackage + "'"); } return targetPackage; } public static final int XML_MODIFIED_FAIL = -1; public static final int XML_MODIFIED_SUCCESS = 0; public static final int XML_MODIFIED_NO_CHANGE = 1; /** * Modify an attribute's value of a certain tag. * * @param xmlFile File, the xml file * @param tagAttributeValueArray, 2-dimension array, the second dimension is 3, containing following * tag String, the tag to be modified * attribute String, the attribute to be modified * value String, the value will be set to attribute * * @return int, XML_MODIFIED_SUCCESS, if the modification has been done successfully. * XML_MODIFIED_FAIL, if the modification fail. * XML_MODIFIED_NO_CHANGE, if no modification is need. */ public static int modifyAndroidManifestXml(File xmlFile, String[][] tagAttributeValueArray){ try { String tag = null;//the tag to be modified String attribute = null;//the attribute to be modifie String value = null;//the value will be set to attribute int noChangeCount = 0; if(xmlFile==null || tagAttributeValueArray==null){ debug("xmlFile or tagAttributeValueArray is null!"); return XML_MODIFIED_FAIL; } Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xmlFile); for(int i=0;i<tagAttributeValueArray.length;i++){ if(tagAttributeValueArray[i]==null || tagAttributeValueArray[i].length!=3){ debug("tagAttributeValueArray["+i+"] is null or its size is not 3."); noChangeCount++; }else{ tag = tagAttributeValueArray[i][0]; attribute = tagAttributeValueArray[i][1]; value = tagAttributeValueArray[i][2]; if(tag==null || attribute==null || value==null){ debug("tag or attribute or value is null!"); noChangeCount++; }else{ NodeList nodes = dom.getElementsByTagName(tag); if (nodes == null || nodes.getLength() == 0) { debug("Can't get Node for tag '" + tag + "'"); return XML_MODIFIED_FAIL; }else{ Node instrumentationNode = nodes.item(0); NamedNodeMap attributes = instrumentationNode.getAttributes(); Node node = attributes.getNamedItem(attribute); if(node==null){ debug("Can't get attribute '" + attribute + "'"); return XML_MODIFIED_FAIL; } if(value.equals(node.getNodeValue())){ debug("'"+value+"'=='"+node.getNodeValue()+"'"); noChangeCount++; } node.setNodeValue(value); } } } } if(noChangeCount==tagAttributeValueArray.length){ return XML_MODIFIED_NO_CHANGE; } //Write dom to xml file // Prepare the DOM document for writing Source source = new DOMSource(dom); // Prepare the output file Result result = new StreamResult(xmlFile); // Write the DOM document to the file Transformer xformer = TransformerFactory.newInstance().newTransformer(); try{ xformer.transform(source, result); }catch(TransformerException e){ result = new StreamResult(new FileOutputStream(xmlFile)); xformer.transform(source, result); } } catch (Exception e) { debug("Met Exception "+e.getMessage()); return XML_MODIFIED_FAIL; } return XML_MODIFIED_SUCCESS; } /** * We use ant to build the apk.<br> * The method will depend on the build.xml, android-sdk-home, ant-home<br> * * * @param appDirString String, the path where apk source locates, an ant build.xml should exists there. * @param debug boolean, true --> build in debug; false --> build in release. * @return boolean, true if the build is successful. */ public static boolean buildAPK(String appDirString, boolean debug){ boolean buildSuccess = false; if(anttool==null) initAntTool(); //create a local.properties file to contain sdk.dir=android-sdk-path //This file will be needed by ant when building the apk String localPropertiesFileString = "local.properties"; File localPropertiesFile = new File(appDirString+File.separator+localPropertiesFileString); if(!localPropertiesFile.exists()){ String androidhome = System.getenv("ANDROID_HOME"); if(androidhome==null){ debug("can't get ANDROID_HOME from environment, please set it"); }else{ if(ConsoleTool.isWindowsOS()){ androidhome = androidhome.replace("\\", "\\\\"); }else{ //TODO Do we need to modify androidhome for other OS??? //this depends the format of file "local.properties" } String sdkProperty = "sdk.dir="+androidhome; PrintWriter wr = null; try { wr = new PrintWriter(new FileWriter(localPropertiesFile)); wr.println(sdkProperty); wr.flush(); } catch (IOException e) { debug("During write to '"+localPropertiesFileString+"', met Exception="+e.getMessage()); }finally{ if(wr!=null) wr.close(); } } }else{ debug(localPropertiesFileString+" file exists."); } //Try to build the apk String[] allArgs = null; int i=0; if(rebuildRunnerAntArgs instanceof String[] && rebuildRunnerAntArgs.length > 0){ allArgs = new String[rebuildRunnerAntArgs.length + 1]; for(String anArg: rebuildRunnerAntArgs) allArgs[i++] = anArg; }else{ allArgs = new String[1]; } allArgs[i] = debug ? "debug" : "release"; Process2 process = null; try { File workingDir = new File(appDirString); process = anttool.ant(workingDir, allArgs).forwardOutput().waitForSuccess(); buildSuccess = true; } catch (Exception e) { debug("During build apk, met Exception="+e.getMessage()); } try{ process.destroy(); process = null;}catch(Exception x){} return buildSuccess; } public static String argsToString(String[] args){ String arguments = ""; for(int i=0;i<args.length;i++){ arguments += args[i]+" "; } return arguments; } /** * User may want to rebuild the TestRunner apk according to the "package" of "aut apk" <br> * * @param testRunnerSourceDir String, the full path where the 'test runner' source is stored. * @param autApk String, the full path of the aut's apk file. * @param testRunnerApk String, the full path of the testrunner's apk file. * @param instrumentArg String, the instrumentation string. * @param debug boolean, true to make a debug build; false to make a release build. * @return String, the full path of the rebuilt apk file.<br> * null, if the rebuild fail.<br> */ public static String rebuildTestRunnerApk(String testRunnerSourceDir, String autApk, String testRunnerApk, String instrumentArg, final boolean debug){ String targetPackage = null; //<manifest package="com.example.android.apis"/> //Find the package's value of tag <manifest> in the AUT's AndroidManifest.xml if(IS_TEST_TARGET_PACKAGE_SET) targetPackage = TEST_TARGET_PACKAGE; else targetPackage = getTargetPackageValue(autApk); if(targetPackage==null || targetPackage.trim().equals("")){ debug("can't get aut's package value."); return null; } //Update AndroidManifest.xml the of TestRunner, the following information may need to be modified //1. <manifest package="com.jayway.android.robotium.remotecontrol.client"> //2. <instrumentation android:name="com.jayway.android.robotium.remotecontrol.client.RobotiumTestRunner"> //3. <instrumentation android:targetPackage="com.example.android.apis"> //Get the AndroidManifest.xml of TestRunner //Find the tag <instrumentation>, replace value of attribute 'android:targetPackage' by local variable targetPackage //Find the tag <instrumentation>, replace value of attribute 'android:name' according to instrumentArg //Find the tag <manifest>, replace value of attribute 'package' according to instrumentArg String instrumentationTag = "instrumentation"; String targetPackageAttribute = "android:targetPackage"; String nameAttribute = "android:name"; String manifestTag = "manifest"; String packageAttribute = "package"; File xmlFile = new File(testRunnerSourceDir+File.separator+MANIFEST_XML_FILENAEM); String[][] tagAttributeValueArray = null; int secondDimensionLength = 1; debug("Modifying xml file '"+xmlFile.getAbsolutePath()+"'."); debug("set attribute '"+targetPackageAttribute+"' to '"+targetPackage+"' for tag '"+instrumentationTag+"'"); String[] instruments = instrumentArg.split("/"); if(instruments.length!=2){ debug("instrument '"+instrumentArg+"' may be wrong."); }else{ secondDimensionLength += 2; debug("package="+instruments[0]+"; instrument's name="+instruments[1]); debug("set attribute '"+packageAttribute+"' to '"+instruments[0]+"' for tag '"+manifestTag+"'"); debug("set attribute '"+nameAttribute+"' to '"+instruments[1]+"' for tag '"+instrumentationTag+"'"); } tagAttributeValueArray = new String[secondDimensionLength][3]; //<instrumentation android:targetPackage="aut.package"> tagAttributeValueArray[0][0] = instrumentationTag; tagAttributeValueArray[0][1] = targetPackageAttribute; tagAttributeValueArray[0][2] = targetPackage; if(secondDimensionLength==3){ //<manifest package="runner.package"> tagAttributeValueArray[1][0] = manifestTag; tagAttributeValueArray[1][1] = packageAttribute; tagAttributeValueArray[1][2] = instruments[0]; //<instrumentation android:name="Runner"> tagAttributeValueArray[2][0] = instrumentationTag; tagAttributeValueArray[2][1] = nameAttribute; tagAttributeValueArray[2][2] = instruments[1]; } int modifyRC = modifyAndroidManifestXml(xmlFile, tagAttributeValueArray); if(XML_MODIFIED_FAIL==modifyRC){ debug("Fail to modify xml file '"+xmlFile.getAbsolutePath()+"'."); return null; }else if(XML_MODIFIED_NO_CHANGE==modifyRC){ if(!rebuildRunnerForce){ debug("No need to rebuild test runner apk. '"+testRunnerApk+"'."); return testRunnerApk; } else{ debug("Forced override for Runner rebuild despite unmodified AndroidManifest.xml."); } } //Rebuild the apk with the modified AndroidManifest.xml debug("Rebuilding the apk with the modified AndroidManifest.xml"); if(!buildAPK(testRunnerSourceDir, debug)){ debug("Fail to build the test runner apk."); return null; }else{ String apkSubDir = "bin"; //Replace testRunnerApk with the rebuilt apk File originalApk = new File(testRunnerApk); File generatedApkFile = new File(testRunnerSourceDir+File.separator+apkSubDir+File.separator+originalApk.getName()); if(!generatedApkFile.exists()){ //we need to get the apk from directory testRunnerSourceDir+File.separator+apkSubDir File builtDir = new File(testRunnerSourceDir+File.separator+apkSubDir); File[] apkfiles = builtDir.listFiles(new FilenameFilter(){ public boolean accept(File dir, String name) { boolean accepted = false; if(name!=null){ String lcname = name.toLowerCase(); if(debug) accepted = lcname.endsWith("debug.apk"); else accepted = lcname.endsWith("release.apk"); } return accepted; } }); if(apkfiles!=null && apkfiles.length>0){ generatedApkFile = apkfiles[0]; } } debug("Replacing testRunnerApk with '"+generatedApkFile.getAbsolutePath()+"'."); if(!generatedApkFile.exists() || !generatedApkFile.isFile()){ debug("Fail to replace testRunnerApk with '"+generatedApkFile.getAbsolutePath()+"'."); return null; } return generatedApkFile.getAbsolutePath(); } } /** * Copy the bytes of one file overwriting or creating a new file. * Since bytes are copied, no character coding issues are expected. * We do no error detection or checking here other than catching Exceptions and * logging the information to debug(). * * @param source File should already be known to exist * @param target File should already be known to be writable. */ public static void copyFile(File source, File target){ try{ byte[] scriptdata = new byte[1024]; FileInputStream in = new FileInputStream(source); FileOutputStream out = new FileOutputStream(target); int read = 0; while(read > -1 && in.available()>0){ read = in.read(scriptdata); if(read > 0){ out.write(scriptdata, 0, read); } } in.close(); out.flush(); out.close(); in = null; out = null; }catch(Exception x){ debug("Copy File Erorr "+ x.getClass().getSimpleName()+":"+ x.getMessage()); } } public static final String RESIGN_JAR_ENV = "RESIGN_JAR_ENV"; public static String getResignJarFileName(){ String resignjar = System.getenv(RESIGN_JAR_ENV); String fullname = null; try{ File file = new File(resignjar); if(file.exists() && file.isFile()){ fullname = file.getAbsolutePath(); } }catch(Exception e){} return fullname; } /** * This method is used to resign AUT {@link #TEST_TARGET_APP} automatically.<br> * Before calling this method, the field {@link #RESIGN_JAR_FULL_NAME} needs to be set.<br> * This method should be called before method {@link #installEnabledAPKs()}<br> * @return true, if the aut is resigned successfully or the aut doesn't need to be resigned. * * @see #TEST_TARGET_APP * @see #RESIGN_JAR_FULL_NAME * @see #installAUT * @see #installEnabledAPKs() */ public static boolean resignAUTApk(){ if(installAUT && RESIGN_JAR_FULL_NAME!=null){ return resignAUTApk(RESIGN_JAR_FULL_NAME, TEST_TARGET_APP); }else{ return true; } } /** * This method is used to resign an android apk.<br> * @param resignJar The full name of re-sign.jar * @param apkFullName The full name of android apk to be resigned. * @return true, if the resign succeed. */ public static boolean resignAUTApk(String resignJar, String apkFullName){ try { Console console = Console.get(); Process2 process = console.batch(null, "java", "-jar", resignJar, apkFullName, apkFullName); process.forwardOutput().waitForSuccess(60).destroy(); process = null; return true; } catch (Exception e) { debug("During resign aut apk, met Exception="+e.getMessage()); return false; } } }