/** ** 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 java.util.HashMap; import java.util.Properties; import java.util.Vector; import org.safs.android.messenger.MessageUtil; import org.safs.android.messenger.MultipleParcelsHandler; import org.safs.android.messenger.client.CommandListener; import org.safs.android.messenger.client.MessageResult; import org.safs.android.messenger.client.MessengerRunner; import org.safs.sockets.DebugListener; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.test.InstrumentationTestRunner; import android.util.Log; import com.jayway.android.robotium.remotecontrol.client.processor.ProcessorInterface; /** * Primary InstrumentationTestRunner used for binding and un-binding TCP Messenger Service.<br> * It contains a MessengerRunner, which is used to handle the messages from 'TCP Messenger'.<br> * When the service is connected, MessengerRunner will be started in a separated thread.<br> * <p> * This main thread will wait there until the thread MessengerRunner's termination.<br> * This class implements the interface CommandListener, which describes the actions to do when <br> * receiving a message from 'TCP Messenger'. The subclass of this class should give a concrete<br> * implementation for methods described in CommandListener.<br> * * @see org.safs.android.messenger.MessengerService * @author Carl Nagle, SAS Institute, Inc. * @since FEB 04, 2012 (CANAGL) Initial version * <br> APR 25, 2013 (LeiWang) Handle message of big size. */ public abstract class AbstractTestRunner extends InstrumentationTestRunner implements CommandListener, DebugListener { public static final String TAG = "AbstractTestRunner"; public static final String resource_service_attached ="SAFS TCP Messenger Attached"; public static final String resource_service_disconnect ="SAFS TCP Messenger Disconnect"; public static final String resource_service_release ="SAFS TCP Messenger Release"; public static final String resource_bind_service ="SAFS TCP Messenger Binding"; protected MessengerRunner messageRunner = null; Object lock = new Object(); boolean runnerStopped = false; /** * Do we need multiple processors for one target? * If not, we may just define the cache as HashMap<String, ProcessorInterface> */ HashMap<String, Vector<ProcessorInterface>> processorsMap = new HashMap<String, Vector<ProcessorInterface>>(); /** * true if we are successfully bound to the TCP Messenger Service. * false if we have disconnected ourselves from the service. * <p> * CAUTION: The Instrumentation thread monitors this to determine if the thread should exit. */ boolean mIsBound = false; /** Exposes the messageRunner.sendServiceResult(Properties) method. */ public boolean sendServiceResult(Properties props){ if(messageRunner != null) return messageRunner.sendServiceResult(props); return false; } /**************************************************************** * In-line ServiceConnection object for local notification of connecting and disconnecting * of the TCP Messenger Service. ****************************************************************/ private ServiceConnection mConnection = new ServiceConnection(){ /** * Upon receiving a bind to the TCP Messenger Service we register our own Messenger * with the service for two-way communication across processes. * @see Messenger#Messenger(IBinder) * @see Message#obtain(Handler, int) * @see AbstractTestRunner#MSG_REGISTER_ENGINE */ public void onServiceConnected(ComponentName className, IBinder service){ Messenger mService = new Messenger(service); debug(resource_service_attached +":"+className); if(messageRunner==null){ messageRunner = new MessengerRunner(mService, AbstractTestRunner.this); messageRunner.start(); messageRunner.sendRegisterEngine(); }else{ debug("Failed!!! "+resource_service_attached +":"+className); } } /** * Destroy our reference to the TCP Messenger Service once disconnected. */ public void onServiceDisconnected(ComponentName className){ if(messageRunner != null){ messageRunner.sendUnRegisterEngine(); debug(resource_service_disconnect+":"+className); messageRunner = null; }else{ debug("Failed!!! "+resource_service_disconnect +":"+className); } } }; private boolean debugEnabled = true; public void setDebugEnabled(boolean enable){ debugEnabled = enable; } public boolean isDebugEnabled(){ return debugEnabled;} public void debug(String message){ if (debugEnabled) Log.d(getListenerName(), message); } /** * Receive debug logging requests from the Messenger Service */ public void onReceiveDebug(String message){ debug(message); } /** * Called when the Instrumentation class is first created. * Here we launch and bind to the TCP Messenger Service and then attempt to deduce the * target Application we are going to be testing. * <p> * We do this through the following sequence * of execution: * <ol> * <li>{@link #doBindService()} * <li>{@link #beforeStart()} * <li>{@link #start()} * <li>{@link #afterStart()} * </ol> */ @Override public void onCreate(Bundle savedInstanceState) { if(!doBindService()){ debug("doBindService Fail! Can't continue."); return; } if(beforeStart()){ start(); afterStart(); } } /** * Called as part of the {@link #onCreate(Bundle)} initialization after {@link #doBindService()} * immediately before {@link #start()}. * <p> * Allows subclasses to do any additional setup prior to starting the Instrumentation Thread as * part of the onCreate call. * @return true to continue normal operation. false to abort and NOT call start(). */ public abstract boolean beforeStart(); /** * Called as part of the {@link #onCreate(Bundle)} initialization after {@link #start()}. * <p> * Allows subclasses to do any additional setup after the Instrumentation Thread has been * started as part of the onCreate call. */ public abstract void afterStart(); /** * Called automatically from start(). * @see #start() */ public void onStart(){ while(mIsBound && !runnerStopped){ synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { debug("Receive InterruptedException during wait()"); } } } finishInstrumentation(); } public void finishInstrumentation(){ //This will terminate the Instrumentation of application try{finish(0, new Bundle());}catch(Throwable x){} } public void messengerRunnerStopped(){ runnerStopped = true; synchronized (lock) { lock.notifyAll(); } } /** * Attempt to force the launch and persistent binding to the separate TCP Messenger Service. * A successful binding will set mIsBound true which will cause our instrumentation to loop * until this is reset to false. */ protected boolean doBindService(){ try{ mIsBound = getContext().bindService(new Intent(MessageUtil.SERVICE_CONNECT_INTENT), mConnection, Context.BIND_AUTO_CREATE); if(mIsBound){ debug(resource_bind_service+":MessengerService"); return true; }else{ debug(resource_bind_service+":UNSUCCESSFUL"); return false; } }catch(Exception x){ debug("doBindService Exception:"+ x.getClass().getSimpleName()+" "+ x.getMessage()); return false; } } /** * Unregister and then unbind with the separate TCP Messenger Service. * This will set reset our mIsBound boolean to false which will allow our instrumentation * thread to shutdown. */ protected boolean doUnbindService(){ if(mIsBound){ try{ getContext().unbindService(mConnection); debug("doUnbindService issuing stopService(shutdown)..."); getContext().stopService(new Intent(MessageUtil.SERVICE_SHUTDOWN_INTENT)); }catch(Exception e){ debug("doUnbindService Exception:"+ e.getClass().getSimpleName()+" "+ e.getMessage()); return false; } mIsBound = false; debug(resource_service_release+":MessengerService"); return true; }else{ debug(resource_service_release+":MessengerService warning, this serviced is not bound."); return true; } } public String getListenerName() { return TAG; } public MessageResult handleEngineShutdown(){ MessageResult result = null; if(doUnbindService()){ result = MessageResult.getSuccessTestResult(result); result.setStatusinfo("Success: "+resource_service_release+":MessengerService"); }else{ result = MessageResult.getFailTestResult(result); result.setStatusinfo("Fail: "+resource_service_release+":MessengerService"); } return result; } /** * =========================================================================================== * Following codes are used to add to, get/remove processor from a cache * =========================================================================================== */ /** * According to the target, put the processor to a cache * * @param target The key with which the processors are stored in cache * @param processor The processor to be stored in cache */ public void addProcessor(String target, ProcessorInterface processor){ Vector<ProcessorInterface> processors = null; if(processorsMap.containsKey(target)){ processors = processorsMap.get(target); processors.add(processor); }else{ processors = new Vector<ProcessorInterface>(); processors.add(processor); processorsMap.put(target, processors); } } /** * According to the target, get the processors from a cache * * @param target The key with which the processors are stored in cache * @return a Vector containing 0 or more ProcessorInterface objects. */ public Vector<ProcessorInterface> getProcessors(String target){ Vector<ProcessorInterface> processors = null; if(processorsMap.containsKey(target)){ processors = processorsMap.get(target); }else{ processors = new Vector<ProcessorInterface>(); } return processors; } /** * Be careful when you call this method, which will remove all the processors from cache */ public void removeProcessors(){ processorsMap.clear(); } /** * Be careful when you call this method, which will remove all the processors * related to 'target' from cache * * @param target The key with which the processors are stored in cache */ public void removeProcessors(String target){ Vector<ProcessorInterface> processors = null; if(processorsMap.containsKey(target)){ processors = processorsMap.get(target); processors.clear(); }else{ debug("The processors cache doesn't contain processors related to '"+target+"'"); } } /** * Remove the processor from the cache, the processor should belong to 'target' * * @param target The key with which the processors are stored in cache * @param processor The processor to be removed from cache */ public void removeProcessor(String target, ProcessorInterface processor){ Vector<ProcessorInterface> processors = null; if(processorsMap.containsKey(target)){ processors = processorsMap.get(target); if(processors.remove(processor)){ debug("Processor '"+processor.getClass().getSimpleName()+"' has been removed."); } }else{ debug("The processors cache doesn't contain processors related to '"+target+"'"); } } /** * =========================================================================================== * Following are the call-back methods inherited from CommandListener * =========================================================================================== */ /** * @param props A Properties object, contains the input parameters including command, target etc.<br> * To get the command, target, use the key defined in SoloMessage, such as KEY_COMMAND,KEY_TARGET<br> * * It also serves as the output for result:<br> * To set the result, use the key defined in SoloMessage, such as KEY_REMOTERESULTCODE<br> * KEY_REMOTERESULTINFO, KEY_ISREMOTERESULT etc.<br> * * @see org.safs.sockets.Message * @see com.jayway.android.robotium.remotecontrol.solo.Message * @see com.jayway.android.robotium.remotecontrol.client.SoloMessage */ public void handleDispatchProps(Properties props) { String debugmsg = getClass().getName()+"handleDispatchProps(): "; String command = null; String target = null; Vector<ProcessorInterface> processors = null; ProcessorInterface processor = null; if(props==null){ debug("Fatal Error: the properties is null"); return; } //Set value for key "isremoteresult"="false" until we know we have executed the command. props.setProperty(SoloMessage.KEY_ISREMOTERESULT, Boolean.toString(false)); //get the target's value target = props.getProperty(SoloMessage.KEY_TARGET); if(target!=null){ debug(debugmsg+" target is '"+target+"'"); processors = getProcessors(target); } //the argument props will take back the result, we don't need the MessageResult command = props.getProperty(SoloMessage.KEY_COMMAND); if(command==null){ props.setProperty(SoloMessage.KEY_REMOTERESULTCODE, SoloMessage.STATUS_REMOTERESULT_FAIL_STRING); props.setProperty(SoloMessage.KEY_REMOTERESULTINFO, SoloMessage.RESULT_INFO_COMMAND_ISNULL); }else{ //Preset the "not executed" result in the properties. //Command will be handled in methods of SoloProcessor, //when the command is properly handled--success or failure--the result will be replaced there. props.setProperty(SoloMessage.KEY_REMOTERESULTCODE, SoloMessage.STATUS_REMOTE_NOT_EXECUTED_STRING); props.setProperty(SoloMessage.KEY_REMOTERESULTINFO, command+SoloMessage.RESULT_INFO_COMMAND_UNKNOWN); boolean processed = false; // cycle through chained target processors only until one of them handles the command for(int i=0; i<processors.size()&& !processed;i++){ processor = processors.get(i); processor.setRemoteCommand(command); processor.processProperties(props); try{processed = ! SoloMessage.STATUS_REMOTE_NOT_EXECUTED_STRING.equals(props.getProperty(SoloMessage.KEY_REMOTERESULTCODE));}catch(NullPointerException x){} } } } }