/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2012 Gorilla Logic, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.gorillalogic.fonemonkey; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import android.app.Activity; import android.app.ActivityManager.RunningAppProcessInfo; import android.content.Context; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.gorillalogic.fonemonkey.automators.AutomationManager; import com.gorillalogic.fonemonkey.automators.IAutomator; import com.gorillalogic.fonemonkey.server.PlaybackServer; import com.gorillalogic.fonemonkey.utils.HttpUtils; import com.gorillalogic.monkeytalk.Command; import com.gorillalogic.monkeytalk.sender.CommandSender; import com.gorillalogic.monkeytalk.sender.CommandSenderFactory; import com.gorillalogic.monkeytalk.server.ServerConfig; public class Recorder { private static PlaybackServer playbackServer; private static CommandSender recordSender; private static BlockingQueue<Command> queue; private static BlockingQueue<Command> remoteRecordingQueue; private static QueueMonitor queueMonitor; static { // Note that this is THE PlaybackServer for the agent -- it catches error when an existing // playback server exists, which prevents the application from crashing. startPlaybackServer(); queue = new LinkedBlockingQueue<Command>(); remoteRecordingQueue = new LinkedBlockingQueue<Command>(); } /** * If an existing Playback server is running in another app, it is shutdown when a record event * is called from AutomatorBase. sendStopCommand is always called because the first time we * start the app we don't know if another app has control of the port and it doesn't hurt * anything to try sending a post request. If this func is called later on in the app then the * goal is to restart the Playback server because another app killed it. * */ private static void startPlaybackServer() { sendStopCommand(); try { playbackServer = new PlaybackServer(); } catch (IOException e) { Log.log("Unable to start playback server", e); } } /** * Stop an existing playback server to free up the port (as defined by * {@link ServerConfig.DEFAULT_PLAYBACK_PORT_ANDROID}). */ private static void sendStopCommand() { // Sending message to another MT application on device. Localhost and default port. URI url = null; try { url = new URI("http", null, "localhost", ServerConfig.DEFAULT_PLAYBACK_PORT_ANDROID, "/", null, null); } catch (URISyntaxException e) { e.printStackTrace(); } // Send the post request, response can be used for debugging new HttpUtils().post(url, "{\"mtcommand\": \"STOP\"}"); } public static PlaybackServer getPlaybackServer() { return playbackServer; } private static boolean recording = false; public static boolean isRecording() { return recording; } public static boolean isPlayingBack() { return !recording; } public synchronized static void setRecording(boolean recording) { Recorder.recording = recording; } public static void setRecordServer(String recordHost, int recordPort) { recordSender = CommandSenderFactory.createCommandSender(recordHost, recordPort); } public static View findViewByResourceID(String resourceID) { for (View root : Recorder.getRoots()) { // Log.log("ROOT = " + root + " RESID = " + resourceID + // " IS SHOWN? " + root.isShown()); if (!root.isShown()) continue; View v = _findViewByResourceID(root, resourceID); if (v != null) return v; } Log.log("RESID " + resourceID + " NOT FOUND IN ANY ROOT"); return null; } private static View _findViewByResourceID(View root, String resourceID) { int id = root.getId(); if (id != View.NO_ID) { String name = root.getContext().getResources().getResourceName(id); if (name.equals(resourceID)) return root; } if (!(root instanceof ViewGroup)) return null; ViewGroup vg = (ViewGroup) root; for (int i = 0; i < vg.getChildCount(); ++i) { View v = _findViewByResourceID(vg.getChildAt(i), resourceID); if (v != null) return v; } return null; } public static View findViewByTextID(String textID) { for (View root : getRoots()) { // Log.log("ROOT = " + root + " RESID = " + resourceID + // " IS SHOWN? " + root.isShown()); if (!root.isShown()) continue; View v = _findViewByTextID(root, textID); if (v != null) return v; } Log.log("TEXTID " + textID + " NOT FOUND IN ANY ROOT"); return null; } private static View _findViewByTextID(View root, String textID) { if (root instanceof TextView) { CharSequence text = ((TextView) root).getText(); // String idName = StringResourceFinder.getIdName(rootActivity, text); String idName = StringResourceFinder .getIdName(AutomationManager.getTopActivity(), text); if (textID.equals(idName)) return root; } if (!(root instanceof ViewGroup)) return null; ViewGroup vg = (ViewGroup) root; for (int i = 0; i < vg.getChildCount(); ++i) { View v = _findViewByTextID(vg.getChildAt(i), textID); if (v != null) return v; } return null; } public static View findViewByItemText(CharSequence itemText) { for (View root : getRoots()) { // Log.log("ROOT = " + root + " IS SHOWN? " + root.isShown()); if (!root.isShown()) continue; View v = _findViewByItemText(root, itemText); if (v != null) return v; } Log.log("ITEMTEXT " + itemText + " NOT FOUND IN ANY ROOT"); return null; } private static View _findViewByItemText(View root, CharSequence itemText) { if (root instanceof TextView) { CharSequence text = ((TextView) root).getText(); if (itemText.equals(text)) return root; } if (!(root instanceof ViewGroup)) return null; ViewGroup vg = (ViewGroup) root; for (int i = 0; i < vg.getChildCount(); ++i) { View v = _findViewByItemText(vg.getChildAt(i), itemText); if (v != null) return v; } return null; } public static View findViewByClassName(String className) { for (View root : getRoots()) { // Log.log("ROOT = " + root + " RESID = " + resourceID + // " IS SHOWN? " + root.isShown()); if (!root.isShown()) continue; View v = _findViewByClassName(root, className); if (v != null) return v; } Log.log("CLASSNAME " + className + " NOT FOUND IN ANY ROOT"); return null; } private static View _findViewByClassName(View root, String className) { if (root.getClass().getName().equals(className)) return root; if (!(root instanceof ViewGroup)) return null; ViewGroup vg = (ViewGroup) root; for (int i = 0; i < vg.getChildCount(); ++i) { View v = _findViewByClassName(vg.getChildAt(i), className); if (v != null) return v; } return null; } public static View findViewByProperty(String methodName, Object customProperty) { for (View root : getRoots()) { // Log.log("ROOT = " + root + " RESID = " + resourceID + // " IS SHOWN? " + root.isShown()); if (!root.isShown()) continue; View v = _findViewByProperty(root, methodName, customProperty); if (v != null) return v; } Log.log("PROPERTY " + customProperty + " NOT FOUND IN ANY ROOT"); return null; } private static View _findViewByProperty(View root, String methodName, Object customProperty) { try { Method[] m = root.getClass().getMethods(); for (int i = 0; i < m.length; ++i) { if (m[i].getName().equals(methodName) && m[i].getParameterTypes().length == 0) { if (m[i].invoke(root, (Object[]) null).equals(customProperty)) return root; } } } catch (SecurityException e) { throw new IllegalStateException(e); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } catch (InvocationTargetException e) { throw new IllegalStateException(e); } if (!(root instanceof ViewGroup)) return null; ViewGroup vg = (ViewGroup) root; for (int i = 0; i < vg.getChildCount(); ++i) { View v = _findViewByProperty(vg.getChildAt(i), methodName, customProperty); if (v != null) return v; } return null; } // TODO: This logic only works for a single ROOT (or if we get lucky). // Need to expand this to include the correct root view in the ordinal. static View findViewByOrdinal(String ordinal) { for (View root : getRoots()) { // Log.log("ORDINAL CHECKING NEXT ROOT"); if (!root.isShown()) continue; View v = _findViewByOrdinal(root, ordinal); if (v != null) return v; } // Log.log("ORDINAL " + ordinal + " NOT FOUND IN ANY ROOT"); return null; } static View _findViewByOrdinal(View root, String ordinal) { // Log.log("ROOT = " + root + " ORD = " + ordinal); if (root == null) return null; if (!(root instanceof ViewGroup)) throw new IllegalStateException("Root view must be ViewGroup"); int n; boolean isLast = false; int dot = ordinal.indexOf('.'); if (dot < 0) { n = Integer.parseInt(ordinal); isLast = true; } else n = Integer.parseInt(ordinal.substring(0, dot)); ViewGroup vg = (ViewGroup) root; View v = vg.getChildAt(n); if (isLast) return v; return _findViewByOrdinal(v, ordinal.substring(dot + 1)); } private static Activity someActivity; // private static Object queueService; public static Activity getSomeActivity() { return someActivity; } public static void setSomeActivity(Activity a) { // ViewGroup vg = getTopMostParent(a.getWindow().getDecorView(), true); // root = a.getWindow().getDecorView().getRootView(); someActivity = a; } public static void prViewTree() { for (View root : getRoots()) { _prViewTree(root, ""); Log.log("-------------------------------------------------------"); } } private static void _prViewTree(View v, String indent) { Log.log(indent + v.getClass().getName()); if (v instanceof ViewGroup) { ViewGroup vg = (ViewGroup) v; for (int i = 0; i < vg.getChildCount(); ++i) { _prViewTree(vg.getChildAt(i), indent + " "); } } } public static void record(String action, IAutomator automator, String[] args) { // Check to ensure another app hasn't killed the Playback server if (!playbackServer.isRunning()) { startPlaybackServer(); } String monkeyId = AutomationManager.findIndexedMonkeyIdIfAny(automator); Command cmd = new Command(automator.getComponentType(), monkeyId, action, (args != null ? Arrays.asList(args) : null), null); recordCommand(cmd); } public static void recordCommand(Command cmd) { // Turn on real-time recording if IDE is reachable if (recordSender == null) { recordSender = CommandSenderFactory.createCommandSender("localhost", ServerConfig.DEFAULT_RECORD_PORT); } if (queueMonitor == null) { queueMonitor = new QueueMonitor(); } if (recording) { // if (queueMonitor == null && queueService == null) { // try { // queueService = new QueueService(CLOUD_RECORD_QUEUE_PORT); // } catch (IOException e) { // Log.log("Unable to enable remote MonkeyTalk IDE recording"); // } // } // Log.log("Recording is on: " + action); Log.log("ENQUEUEING - " + cmd); try { queue.put(cmd); } catch (InterruptedException ex) { Log.log(ex); } } else { // Log.log("Recording is off: " + action); } } // ///////////////////////////////////////////////////////////////////////////////////// public static boolean isAppInForeground() { try { android.app.ActivityManager activityManager = (android.app.ActivityManager) someActivity .getSystemService(Context.ACTIVITY_SERVICE); List<RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses(); if (appProcesses == null) { return false; } final String packageName = someActivity.getPackageName(); // ? for (RunningAppProcessInfo appProcess : appProcesses) { if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) { return true; } } return false; } catch (Exception e) { Log.log(e); } return false; } private static Set<View> getRoots() { return FunctionalityAdder.getRoots(); } public static class QueueMonitor { public QueueMonitor() { new Thread(new Runnable() { @Override public void run() { try { while (true) { Command cmd = queue.take(); Log.log("SENDING - " + cmd); // Response resp = recordSender.record(cmd); // if (resp.getCode() != 200) { // Log.log("Unable to record: " // + cmd.getCommand() // + // " - If you're running on a device, you must connect to Wi-Fi. You cannot record over a tether: " // + resp.getMessage()); if (remoteRecordingQueue == null) { Log.log("RRQ Null"); } if (cmd == null) { Log.log("CMD Null"); } // try { // remoteRecordingQueue.put(cmd); // } catch (Exception e) { // Log.log("Remote Recording Queue error", e); // } remoteRecordingQueue.put(cmd); } // } } catch (InterruptedException ex) { Log.log(ex); } } }).start(); } } public synchronized static List<Command> pollQueue() { List<Command> list = new ArrayList<Command>(); Command c = null; while (!remoteRecordingQueue.isEmpty()) { try { // Block until there's something in the queue c = remoteRecordingQueue.take(); list.add(c); } catch (InterruptedException e) { // } // Keep reading until empty } return list; } public static void clearQueue() { Recorder.remoteRecordingQueue.clear(); } }