/** ** Copyright (C) SAS Institute, All rights reserved. ** General Public License: http://www.opensource.org/licenses/gpl-license.php **/ package com.jayway.android.robotium.remotecontrol.client; import org.safs.android.messenger.client.MessageResult; import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import com.jayway.android.robotium.remotecontrol.client.processor.SoloProcessor; import com.jayway.android.robotium.remotecontrol.solo.Message; import com.jayway.android.robotium.solo.RCSolo; /** * Primary InstrumentationTestRunner used for remote controlled Android Automation in associated * with a TCP Messenger Service. * <p> * This is the InstrumentationTestRunner that is considered the test package usually associated with * a very specific target Package to be tested. However, in the case of SAFS we want to make a completely * reusable test package that is NOT built with a specific target Package association. We want this * general-purpose test framework to be usable to test all Android Applications via the data-driven * <a href="http://safsdev.sourceforge.net" target="_blank">SAFS framework</a>. * <p> * The initial implementation, however, is not necessarily SAFS-specific. * <p> * How the remote control mechanism works:<br> * The test package AdroidManifest.xml does need to have the Instrumentation tags set for the target * Application to be tested. There is no getting around this: * <p><pre> * <instrumentation android:name="com.jayway.android.robotium.remotecontrol.client.RobotiumTestRunner" * android:targetPackage="com.android.example.spinner" * android:label="General-Purpose SAFS Droid Automation Framework"/> * </pre> * The AndroidManifest.xml then simply needs to be repackaged into a working test APK in order to test * the target application. * <p> * The test consists of the following on-device assets:<br> * 1. TCP Messenger Service,<br> * 2. Robotium Remote Solo APK,<br> * 3. Target Application APK.<br> * <p> * The remote control assets are simply:<br> * 1. Remote Controller implementing a TCP SocketServerListener,<br> * 2. TCP SocketServer binding to the on-device TCP Messenger Service for two-way communication.<br> * <p> * There is a predefined TCP Protocol the remote TCP SocketServer and the on-device TCP Messenger Service * must adhere to for proper signalling and synchronization. * <p> * When using the Droid Emulator, the remote controller must ensure the proper emulator port forwarding is * set in order for the TCP Messenger Service to be able to communicate with the outside world. * * @see org.safs.android.messenger.MessengerService * @see com.jayway.android.robotium.remotecontrol.client.AbstractTestRunner * @author Carl Nagle, SAS Institute, Inc. * <br>May 21, 2013 (SBJLWA) Use RCSolo instead of Solo. */ public class RobotiumTestRunner extends AbstractTestRunner { public static String TAG = "RobotiumTestRunner"; PackageManager myPackageManager = null; /** * Flags OR'd together to retrieve all available information on an installed Package. */ int ALL_PACKAGE_INFO = PackageManager.GET_ACTIVITIES | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_GIDS | PackageManager.GET_INSTRUMENTATION | PackageManager.GET_PERMISSIONS | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_SERVICES | PackageManager.GET_SIGNATURES ; /** * Flags OR'd together to retrieve all available information on an Application. */ int ALL_APPLICATION_INFO = PackageManager.GET_META_DATA | PackageManager.GET_SHARED_LIBRARY_FILES | PackageManager.GET_UNINSTALLED_PACKAGES; PackageInfo myPackageInfo = null; InstrumentationInfo myInstrumentInfo = null; String targetPackageString = null; Intent targetLaunchIntent = null; String targetApplicationClassName = null; PackageInfo targetPackageInfo = null; ActivityInfo[] targetActivityInfo = null; RobotiumTestCase activityrunner = null; Activity mainApp = null; RCSolo solo = null; public RobotiumTestRunner(){ super(); addProcessor(SoloMessage.target_solo, initializeSoloProcessor()); } /** * Subclasses will want to override to instantiate their own SoloProcessor subclass.<br> * This Processor will be add to processors-cache with key {@link SoloMessage#target_solo} * * @see SoloMessage#target_solo * @param runner * @return */ public SoloProcessor initializeSoloProcessor(){ return new SoloProcessor(this); } public RobotiumTestCase getActivityrunner() { return activityrunner; } /** * The returned {@link RCSolo} is probably a null.<br> * The {@link RCSolo} object will be initialized after calling {@link #launchApplication()}<br> * <p> * We CANNOT automatically call launchApplication as part of this method because there * are cases--especially during initialization--when the call to launchApplication will * most definitely fail. * * @return {@link com.jayway.android.robotium.solo.RCSolo} or null if the application * has not yet been launched by the test/user at an appropriate time. */ public RCSolo getSolo() { return solo; } @Override public String getListenerName() { return TAG; } /** * Instantiate the RobotiumTestCase. Subclasses will likely override. * Called as part of the beforeStart initialization AFTER getTargetPackageInfo has initialized * some critical fields. * * @see RobotiumTestCase * @see #beforeStart() * @see #getTargetPackageInfo() */ boolean createRobotiumTestCase(){ debug("createTestCase commencing..."); try { //Using the extracted activity class name targetApplicationClassName to instantiate a TestCase activityrunner = new RobotiumTestCase(targetPackageString, Class.forName(targetApplicationClassName)); } catch (ClassNotFoundException e) { debug("ClassNotFoundException: "+targetApplicationClassName+" can't be found."); return false; } debug("createTestCase COMPLETE."); return true; } /** * Attempt to initialize the RobotiumTestCase with Intent and Instrumentation. * Normally, subclasses would not need to override this method. * * @see RobotiumTestCase#setActivityIntent(Intent) * @see RobotiumTestCase#injectInstrumentation(android.app.Instrumentation) */ boolean initializeInstrumentation(){ debug("initializeInstrumentation commencing..."); activityrunner.setActivityIntent(targetLaunchIntent); //Inject the Instrumentation to TestCase activityrunner.injectInstrumentation(this); debug("initializeInstrumentation COMPLETE."); return true; } /** * Attempt to extract all pertinent information for launching/driving the package we are testing. * * @see PackageManager#getPackageInfo(String, int) * @see PackageInfo#instrumentation * @see InstrumentationInfo#targetPackage * @see PackageManager#getLaunchIntentForPackage(String) */ boolean getTargetPackageInfo(){ myPackageManager = getContext().getPackageManager(); try{ myPackageInfo = myPackageManager.getPackageInfo(getContext().getPackageName(), PackageManager.GET_INSTRUMENTATION ); myInstrumentInfo = myPackageInfo.instrumentation[0]; targetPackageString = myInstrumentInfo.targetPackage; debug("Found target Package: "+ targetPackageString); targetPackageInfo = myPackageManager.getPackageInfo(targetPackageString, ALL_PACKAGE_INFO); targetLaunchIntent = myPackageManager.getLaunchIntentForPackage(targetPackageString); targetLaunchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try{ debug("Found Launch Intent Action: "+ targetLaunchIntent.getAction() ); debug("Is ACTION_MAIN : "+ targetLaunchIntent.getAction().equals(Intent.ACTION_MAIN)); targetApplicationClassName = targetLaunchIntent.getComponent().getClassName(); debug("Found Launch Component: "+ targetApplicationClassName); }catch(NullPointerException np){ debug("Target Launch Intent is NULL!"); } targetActivityInfo = targetPackageInfo.activities; try{ debug("Found "+ targetActivityInfo.length +" target Activities."); for(int i=0;i<targetActivityInfo.length;i++){ debug(" Activity Name: "+ targetActivityInfo[i].name); } }catch(NullPointerException np){ debug("Found 0 target Activities."); } return true; }catch(NameNotFoundException nf){ debug("getTargetPackage "+nf.getClass().getSimpleName()+", "+ nf.getMessage()); return false; } } @Override public boolean beforeStart(){ boolean processok = true; processok &= getTargetPackageInfo(); processok &= createRobotiumTestCase(); processok &= initializeInstrumentation(); return processok; } @Override public void afterStart() { } /** * Launches the main Application Activity and initializes the Robotium Solo object. * This method MUST be invoked firstly before we can use Robotium Solo object to do further work. * * @see RobotiumTestCase#setUp() * @see RobotiumTestCase#getRobotium() */ public void launchApplication(){ debug("launchApplication commencing..."); activityrunner.setUp(); solo = activityrunner.getRobotium(); debug("launchApplication COMPLETE."); } /** * Force a shutdown of ANY running test Activities. * This is done via our Robotium Solo instance. */ public void closeApplication(){ if(solo==null){ debug("Error: The solo has not been initialized!"); return; } solo.finishOpenedActivities(); } /** * =========================================================================================== * Following are the call-back methods inheritated from CommandListener * =========================================================================================== */ public MessageResult handleDispatchFile(String filename) { // TODO: return null; } public MessageResult handleMessage(String message) { // DEBUG Proof-of-concept: // this will not normally have a command implementation here // "launch" the launcher activity if(message.equalsIgnoreCase("launch")){ debug("Handler processing LaunchApplication of "+ targetApplicationClassName); try{ launchApplication(); debug("LaunchApplication of "+ targetApplicationClassName +" sending Results."); messageRunner.sendServiceResult(Message.STATUS_REMOTERESULT_OK, "We should have launched "+targetApplicationClassName); }catch(Throwable x){ debug("LaunchApplication "+ x.getClass().getSimpleName()+" "+x.getMessage()); x.printStackTrace(); messageRunner.sendServiceResult(Message.STATUS_REMOTERESULT_UNKNOWN, "We failed to LAUNCH "+targetApplicationClassName); } } // DEBUG Proof-of-concept: // this will not normally have a command implementation here // "close" the application else if(message.equalsIgnoreCase("close")){ debug("Handler processing CloseApplication of "+ targetApplicationClassName); try{ closeApplication(); debug("CloseApplication of "+ targetApplicationClassName +" sending Results."); messageRunner.sendServiceResult(Message.STATUS_REMOTERESULT_OK, "We should have closed "+targetApplicationClassName); }catch(Throwable x){ debug("CloseApplication "+ x.getClass().getSimpleName()+" "+x.getMessage()); x.printStackTrace(); messageRunner.sendServiceResult(Message.STATUS_REMOTERESULT_UNKNOWN, "We failed to CLOSE "+targetApplicationClassName); } } // any other unrecognized message is passed thru else{ debug("Handler Received Custom message: "+ message); // TODO: where to? messageRunner.sendServiceResult(Message.STATUS_REMOTERESULT_OK, "Received custom message "+message); } return null; } public MessageResult handleServerConnected() { // TODO Auto-generated method stub return null; } public MessageResult handleServerDisconnected() { // TODO return null; } public MessageResult handleServerShutdown() { // TODO Auto-generated method stub return null; } public MessageResult handleRemoteShutdown() { // TODO Auto-generated method stub return null; } }