/* * Copyright (C) 2012 Baidu.com Inc * * 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.baidu.cafe.local.record; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Queue; import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.os.Build; import android.os.SystemClock; import android.text.Editable; import android.text.TextWatcher; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.EditText; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnGroupClickListener; import android.widget.ScrollView; import android.widget.Spinner; import com.baidu.cafe.utils.ReflectHelper; /** * A single class transplaned from com.baidu.cafe.local.record.ViewRecorder * which is not depended on com.baidu.cafe.local.Locallib. * * Usage: * * { * * super.onCreate(); * * ViewRecorderSDK vr = new ViewRecorderSDK(this); * * vr.beginRecordCode(); * * vr.pollOutputLogQueue(); * * } * * @author luxiaoyu01@baidu.com * @date 2013-10-8 * @version * @todo */ public class ViewRecorderSDK { private final static int MAX_SLEEP_TIME = 20000; private final static int MIN_SLEEP_TIME = 1000; private final static int MIN_STEP_COUNT = 4; private final static boolean DEBUG_WEBVIEW = true; private final static SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss.SSS"); private static boolean mBegin = false; /** * For judging whether a view is an old one. * * Key is string of view id. * * Value is position array of view. */ private HashMap<String, int[]> mAllViewPosition = new HashMap<String, int[]>(); /** * For judging whether a view has been hooked. */ private ArrayList<Integer> mAllListenerHashcodes = new ArrayList<Integer>( 1024); private ArrayList<Integer> mAllAbsListViewHashcodes = new ArrayList<Integer>(); /** * For judging whether a EditText has been hooked. */ private ArrayList<EditText> mAllEditTexts = new ArrayList<EditText>(); /** * For merge a sequeue of MotionEvents to a drag. */ private Queue<RecordMotionEvent> mMotionEventQueue = new LinkedList<RecordMotionEvent>(); /** * For judging events of the same view at the same time which should be * keeped by their priorities. */ private Queue<OutputEvent> mOutputEventQueue = new LinkedList<OutputEvent>(); /** * For saving output log */ private Queue<String> mOutputLogQueue = new LinkedList<String>(); /** * For mapping keycode to keyname */ private HashMap<Integer, String> mKeyCodeMap = new HashMap<Integer, String>(); /** * lock for OutputEventQueue * * NOTICE: new String("") can not replaced by "", because the code * synchronizes on interned String. Constant Strings are interned and shared * across all other classes loaded by the JVM. Thus, this could is locking * on something that other code might also be locking. This could result in * very strange and hard to diagnose blocking and deadlock behavior. */ private static String mSyncOutputEventQueue = new String( "mSyncOutputEventQueue"); /** * lock for MotionEventQueue */ private static String mSyncMotionEventQueue = new String( "mSyncMotionEventQueue"); /** * lock for OutputLogQueue */ private static String mSyncOutputLogQueue = new String( "mSyncOutputLogQueue"); /** * lock for AllListenerHashcodes */ private static String mSyncAllListenerHashcodes = new String( "mSyncAllListenerHashcodes"); /** * Time when event was being generated. */ private long mTheCurrentEventOutputime = System.currentTimeMillis(); /** * event count for naming screenshot */ private int mEventCount = 0; /** * interval between events */ private long mLastEventTime = System.currentTimeMillis(); /** * assume that only one ScrollView is fling */ private String mFamilyStringBeforeScroll = ""; /** * to ignore drag event */ private boolean mIsLongClick = false; private boolean mDragWithoutUp = false; /** * to ignore drag event when "output a drag without up" */ private boolean mIsAbsListViewToTheEnd = false; /** * Saving states for each listview */ private HashMap<String, AbsListViewState> mAbsListViewStates = new HashMap<String, AbsListViewState>(); /** * save edittext the lastest text */ private HashMap<String, String> mEditTextLastText = new HashMap<String, String>(); private HashMap<OnClickListener, Integer> mOnClickListenerInvokeCounter = new HashMap<OnClickListener, Integer>(); private HashMap<OnTouchListener, Integer> mOnTouchListenerInvokeCounter = new HashMap<OnTouchListener, Integer>(); private HashMap<OnKeyListener, Integer> mOnKeyListenerCounters = new HashMap<OnKeyListener, Integer>(); private HashMap<OnScrollListener, Integer> mOnScrollListenerCounters = new HashMap<OnScrollListener, Integer>(); private HashMap<OnGroupClickListener, Integer> mOnGroupClickListenerCounters = new HashMap<OnGroupClickListener, Integer>(); private HashMap<OnChildClickListener, Integer> mOnChildClickListenerCounters = new HashMap<OnChildClickListener, Integer>(); /** * Saving old listener for invoking when needed */ private HashMap<String, OnClickListener> mOnClickListeners = new HashMap<String, OnClickListener>(); private HashMap<String, OnLongClickListener> mOnLongClickListeners = new HashMap<String, OnLongClickListener>(); private HashMap<String, OnTouchListener> mOnTouchListeners = new HashMap<String, OnTouchListener>(); private HashMap<String, OnKeyListener> mOnKeyListeners = new HashMap<String, OnKeyListener>(); private HashMap<String, OnItemClickListener> mOnItemClickListeners = new HashMap<String, OnItemClickListener>(); private HashMap<String, OnGroupClickListener> mOnGroupClickListeners = new HashMap<String, OnGroupClickListener>(); private HashMap<String, OnChildClickListener> mOnChildClickListeners = new HashMap<String, OnChildClickListener>(); private HashMap<String, OnScrollListener> mOnScrollListeners = new HashMap<String, OnScrollListener>(); private HashMap<String, OnItemLongClickListener> mOnItemLongClickListeners = new HashMap<String, OnItemLongClickListener>(); private String mPackageName = null; private int mCurrentEditTextIndex = 0; private String mCurrentEditTextString = ""; private boolean mHasTextChange = false; private long mTheLastTextChangedTime = System.currentTimeMillis(); private int mCurrentScrollState = 0; private Context mContext = null; private ActivityManager mActivityManager = null; public ViewRecorderSDK(Context context) { this.mContext = context; mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); init(); } public class OutputEvent { final static int PRIORITY_DRAG = 1; final static int PRIORITY_KEY = 2; final static int PRIORITY_SCROLL = 3; final static int PRIORITY_CLICK = 4; final static int PRIORITY_WEBELEMENT_CLICK = 10; final static int PRIORITY_WEBELEMENT_CHANGE = 10; /** * NOTICE: This field can not be null! */ public View view = null; public int priority = 0; protected String code = ""; protected String log = ""; public String getCode() { return code; } public String getLog() { return log; } public void setCode(String code) { this.code = code; } public void setLog(String log) { this.log = log; } @Override public String toString() { return String.format("[%s] %s", view, priority); } @Override public boolean equals(Object o) { if (null == o) { return false; } OutputEvent target = (OutputEvent) o; return this.view.equals(target.view) && this.priority == target.priority ? true : false; } } class RecordMotionEvent { public View view; public float x; public float y; public int action; public long time; public RecordMotionEvent(View view, int action, float x, float y, long time) { this.view = view; this.x = x; this.y = y; this.action = action; this.time = time; } @Override public String toString() { return String .format("RecordMotionEvent(%s, action=%s, x=%s, y=%s)", view, action, x, y); } } class AbsListViewState { public int firstVisibleItem = 0; public int visibleItemCount = 0; public int totalItemCount = 0; public int lastFirstVisibleItem = 0; } class ClickEvent extends OutputEvent { public ClickEvent(View view) { this.view = view; this.priority = PRIORITY_CLICK; } } class DragEvent extends OutputEvent { public DragEvent(View view) { this.view = view; this.priority = PRIORITY_DRAG; } } class HardKeyEvent extends OutputEvent { public HardKeyEvent(View view) { this.view = view; this.priority = PRIORITY_KEY; } } class ScrollEvent extends OutputEvent { public ScrollEvent(View view) { this.view = view; this.priority = PRIORITY_SCROLL; } } /** * sort by view.hashCode() */ class SortByView implements Comparator<OutputEvent> { @Override public int compare(OutputEvent e1, OutputEvent e2) { if (null == e1 || null == e1.view) { return -1; } if (null == e2 || null == e2.view) { return 1; } if (e1.view.hashCode() > e2.view.hashCode()) { return 1; } return -1; } } /** * sort by proity */ class SortByPriority implements Comparator<OutputEvent> { @Override public int compare(OutputEvent e1, OutputEvent e2) { if (null == e1 || null == e1.view) { return -1; } if (null == e2 || null == e2.view) { return 1; } if (e1.priority > e2.priority) { return 1; } return -1; } } /** * sort by view.familyString.length() */ class SortByFamilyString implements Comparator<OutputEvent> { @Override public int compare(OutputEvent e1, OutputEvent e2) { if (null == e1 || null == e1.view) { return -1; } if (null == e2 || null == e2.view) { return 1; } // longer means younger if (getFamilyString(e1.view).length() > getFamilyString(e2.view).length()) { return 1; } return -1; } } private final static String CLASSNAME_DECORVIEW = "com.android.internal.policy.impl.PhoneWindow$DecorView"; private String getFamilyString(View v) { View view = v; String familyString = ""; while (view.getParent() instanceof ViewGroup) { ViewGroup parent = (ViewGroup) view.getParent(); if (null == parent) { printLog("null == parent at getFamilyString"); return rmTheLastChar(familyString); } if (Build.VERSION.SDK_INT >= 14 && parent.getClass().getName().equals(CLASSNAME_DECORVIEW)) { } else { familyString += getChildIndex(parent, view) + "-"; } view = parent; } return rmTheLastChar(familyString); } private int getChildIndex(ViewGroup parent, View child) { int countInvisible = 0; for (int i = 0; i < parent.getChildCount(); i++) { if (parent.getChildAt(i).equals(child)) { return i - countInvisible; } if (parent.getChildAt(i).getVisibility() != View.VISIBLE) { countInvisible++; } } return -1; } private String rmTheLastChar(String str) { return str.length() == 0 ? str : str.substring(0, str.length() - 1); } private String getViewText(View view) { try { Method method = view.getClass().getMethod("getText"); return (String) (method.invoke(view)); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { // eat it } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (ClassCastException e) { // eat it } return ""; } private void print(String tag, String message) { if ("RecorderCode".equals(tag)) { offerOutputLogQueue(message); } Log.i(tag, message); } private void printLog(String message) { print("ViewRecorder", message); } private void printCode(String message) { print("RecorderCode", message); } private void init() { setWindowManagerString(); mPackageName = mContext.getPackageName(); //((Activity)context).getWindowManager(); initKeyTable(); } /** * Add listeners on all views for generating cafe code automatically */ public void beginRecordCode() { if (mBegin) { printLog("ViewRecorderSDK has already begin!"); return; } mBegin = true; monitorCurrentActivity(); new Thread(new Runnable() { public void run() { while (true) { sleep(50); ArrayList<View> newViews = getTargetViews(); if (newViews.size() == 0) { continue; } setDefaultFocusView(); for (View view : newViews) { try { setHookListenerOnView(view); } catch (Exception e) { e.printStackTrace(); } } } } }, "keep hooking new views").start(); System.out.println("ViewRecorder is ready to work."); handleRecordMotionEventQueue(); handleOutputEventQueue(); mLastEventTime = System.currentTimeMillis(); printLog("ViewRecorder is ready to work."); } private void sleep(int time) { try { Thread.sleep(time); } catch (InterruptedException ignored) { } } private void monitorCurrentActivity() { new Thread(new Runnable() { @Override public void run() { while (true) { updateCurrentActivity(); sleep(1000); } } }, "monitorCurrentActivity").start(); } /** * @return new activity class */ private void updateCurrentActivity() { // Activity activity = local.getCurrentActivity(); setOnTouchListenerOnDecorView(); } private static Class<?> windowManager; static { try { String windowManagerClassName; if (android.os.Build.VERSION.SDK_INT >= 17) { windowManagerClassName = "android.view.WindowManagerGlobal"; } else { windowManagerClassName = "android.view.WindowManagerImpl"; } windowManager = Class.forName(windowManagerClassName); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (SecurityException e) { e.printStackTrace(); } } private String windowManagerString; /** * Sets the window manager string. */ private void setWindowManagerString() { if (android.os.Build.VERSION.SDK_INT >= 17) { windowManagerString = "sDefaultWindowManager"; } else if (android.os.Build.VERSION.SDK_INT >= 13) { windowManagerString = "sWindowManager"; } else { windowManagerString = "mWindowManager"; } } private View[] getWindowDecorViews() { Field viewsField; Field instanceField; try { viewsField = windowManager.getDeclaredField("mViews"); instanceField = windowManager.getDeclaredField(windowManagerString); viewsField.setAccessible(true); instanceField.setAccessible(true); Object instance = instanceField.get(null); return (View[]) viewsField.get(instance); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } private ArrayList<View> getViews() { try { return getViews(null, false); } catch (Exception e) { e.printStackTrace(); return null; } } /** * Extracts all {@code View}s located in the currently active * {@code Activity}, recursively. * * @param parent * the {@code View} whose children should be returned, or * {@code null} for all * @param onlySufficientlyVisible * if only sufficiently visible views should be returned * @return all {@code View}s located in the currently active * {@code Activity}, never {@code null} */ private ArrayList<View> getViews(View parent, boolean onlySufficientlyVisible) { final ArrayList<View> views = new ArrayList<View>(); final View parentToUse; if (parent == null) { return getAllViews(onlySufficientlyVisible); } else { parentToUse = parent; views.add(parentToUse); if (parentToUse instanceof ViewGroup) { addChildren(views, (ViewGroup) parentToUse, onlySufficientlyVisible); } } return views; } /** * Adds all children of {@code viewGroup} (recursively) into {@code views}. * * @param views * an {@code ArrayList} of {@code View}s * @param viewGroup * the {@code ViewGroup} to extract children from * @param onlySufficientlyVisible * if only sufficiently visible views should be returned */ private void addChildren(ArrayList<View> views, ViewGroup viewGroup, boolean onlySufficientlyVisible) { if (viewGroup != null) { for (int i = 0; i < viewGroup.getChildCount(); i++) { final View child = viewGroup.getChildAt(i); if (onlySufficientlyVisible && isViewSufficientlyShown(child)) views.add(child); else if (!onlySufficientlyVisible) views.add(child); if (child instanceof ViewGroup) { addChildren(views, (ViewGroup) child, onlySufficientlyVisible); } } } } /** * Returns true if the view is sufficiently shown * * @param view * the view to check * @return true if the view is sufficiently shown */ private final boolean isViewSufficientlyShown(View view) { final int[] xyView = new int[2]; final int[] xyParent = new int[2]; if (view == null) return false; final float viewHeight = view.getHeight(); final View parent = getScrollOrListParent(view, 1); view.getLocationOnScreen(xyView); if (parent == null) { xyParent[1] = 0; } else { parent.getLocationOnScreen(xyParent); } if (xyView[1] + (viewHeight / 2.0f) > getScrollListWindowHeight(view)) return false; else if (xyView[1] + (viewHeight / 2.0f) < xyParent[1]) return false; return true; } /** * Returns the height of the scroll or list view parent * * @param view * the view who's parents height should be returned * @return the height of the scroll or list view parent */ @SuppressWarnings("deprecation") private float getScrollListWindowHeight(View view) { final int[] xyParent = new int[2]; View parent = getScrollOrListParent(view, 1); final float windowHeight; if (parent == null) { windowHeight = ((Activity) mContext).getWindowManager().getDefaultDisplay().getHeight(); } else { parent.getLocationOnScreen(xyParent); windowHeight = xyParent[1] + parent.getHeight(); } parent = null; return windowHeight; } /** * Returns the scroll or list parent view * * @param view * the view who's parent should be returned * @return the parent scroll view, list view or null */ private View getScrollOrListParent(View view, int depth) { depth++; if (!(view instanceof android.widget.AbsListView) && !(view instanceof android.widget.ScrollView) && !(view instanceof WebView)) { try { return getScrollOrListParent((View) view.getParent(), depth); } catch (Exception e) { return null; } } else { return view; } } private final View[] getNonDecorViews(View[] views) { View[] decorViews = null; if (views != null) { decorViews = new View[views.length]; int i = 0; View view; for (int j = 0; j < views.length; j++) { view = views[j]; if (view != null && !(view.getClass().getName() .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) { decorViews[i] = view; i++; } } } return decorViews; } private ArrayList<View> getAllViews(boolean onlySufficientlyVisible) { final View[] views = getWindowDecorViews(); final ArrayList<View> allViews = new ArrayList<View>(); final View[] nonDecorViews = getNonDecorViews(views); View view = null; if (nonDecorViews != null) { for (int i = 0; i < nonDecorViews.length; i++) { view = nonDecorViews[i]; try { addChildren(allViews, (ViewGroup) view, onlySufficientlyVisible); } catch (Exception ignored) { } if (view != null) allViews.add(view); } } if (views != null && views.length > 0) { view = getRecentDecorView(views); try { addChildren(allViews, (ViewGroup) view, onlySufficientlyVisible); } catch (Exception ignored) { } if (view != null) allViews.add(view); } return allViews; } /** * Returns the most recent DecorView * * @param views * the views to check * @return the most recent DecorView */ private final View getRecentDecorView(View[] views) { final View[] decorViews = new View[views.length]; int i = 0; View view; for (int j = 0; j < views.length; j++) { view = views[j]; if (view != null && view.getClass().getName() .equals("com.android.internal.policy.impl.PhoneWindow$DecorView")) { decorViews[i] = view; i++; } } return getRecentContainer(decorViews); } /** * Returns the most recent view container * * @param views * the views to check * @return the most recent view container */ private final View getRecentContainer(View[] views) { View container = null; long drawingTime = 0; View view; for (int i = 0; i < views.length; i++) { view = views[i]; if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) { container = view; drawingTime = view.getDrawingTime(); } } return container; } /** * If there is no views to handle onTouch event, decorView will handle it * and invoke activity.onTouchEvent(event).If decorView does not handle a * touch event by return true, events follow-up will not be dispatched to * views including decorView. */ private void setOnTouchListenerOnDecorView() { View[] views = getWindowDecorViews(); if (views != null) { for (View view : views) { handleOnTouchListener(view); } } else { printLog("setOnTouchListenerOnDecorView NULL pointer."); } // View decorView = activity.getWindow().getDecorView(); } private float getDisplayX() { DisplayMetrics dm = new DisplayMetrics(); ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm); //mActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); return dm.widthPixels; } private float getDisplayY() { DisplayMetrics dm = new DisplayMetrics(); ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm); return dm.heightPixels; } private boolean isInScreen(View view) { int[] location = new int[2]; view.getLocationOnScreen(location); int leftX = location[0]; int righX = location[0] + view.getWidth(); int leftY = location[1]; int righY = location[1] + view.getHeight(); return righX < 0 || leftX > getDisplayX() || righY < 0 || leftY > getDisplayY() ? false : true; } private boolean isSize0(final View view) { return view.getHeight() == 0 || view.getWidth() == 0; } private <T extends View> ArrayList<T> removeInvisibleViews(ArrayList<T> viewList) { ArrayList<T> tmpViewList = new ArrayList<T>(viewList.size()); for (T view : viewList) { if (view != null && view.isShown() && isInScreen(view) && !isSize0(view)) { tmpViewList.add(view); } } return tmpViewList; } private ArrayList<View> getTargetViews() { ArrayList<View> views = removeInvisibleViews(getViews(null, false)); // ArrayList<View> views = local.getViews(); ArrayList<View> targetViews = new ArrayList<View>(); for (View view : views) { // for thread safe if (null == view) { continue; } boolean isOld = mAllViewPosition.containsKey(getViewID(view)); // refresh view layout if (hasChange(view)) { saveView(view); } if (!isOld) { // save new view saveView(view); targetViews.add(view); handleOnKeyListener(view); } else { // get view who have unhooked listeners if (hasUnhookedListener(view)) { targetViews.add(view); } } } return targetViews; } private void saveView(View view) { if (null == view) { printLog("null == view "); return; } String viewID = getViewID(view); int[] xy = new int[2]; view.getLocationOnScreen(xy); mAllViewPosition.put(viewID, xy); } private boolean hasChange(View view) { // new view String viewID = getViewID(view); int[] oldXy = mAllViewPosition.get(viewID); if (null == oldXy) { return true; } // location change int[] xy = new int[2]; view.getLocationOnScreen(xy); return xy[0] != oldXy[0] || xy[1] != oldXy[1] ? true : false; } private View getFocusView(ArrayList<View> views) { for (View view : views) { if (view.isFocused()) { return view; } } return null; } private View getCurrentFocusView() { ArrayList<View> views = getViews(); return getFocusView(views); } private View getRecentDecorView() { View[] views = getWindowDecorViews(); if (null == views || 0 == views.length) { printLog("0 == views.length at getRecentDecorView"); return null; } View recentDecorview = getRecentDecorView(views); if (null == recentDecorview) { // print("null == rview; use views[0]: " + views[0]); recentDecorview = views[0]; } return recentDecorview; } private void setDefaultFocusView() { // It's too slow.. // if (local.getCurrentActivity().getCurrentFocus() != null) { // return; // } if (getCurrentFocusView() != null) { return; } View view = getRecentDecorView(); if (null == view) { printLog("null == view of setDefaultFocusView"); return; } // boolean hasFocus = local.requestFocus(view); // printLog(view + " hasFocus: " + hasFocus); String viewID = getViewID(view); if (!mAllViewPosition.containsKey(viewID)) { saveView(view); handleOnKeyListener(view); } } private boolean hasUnhookedListener(View view) { String[] listenerNames = new String[] { "mOnItemClickListener", "mOnClickListener", "mOnTouchListener", "mOnKeyListener", "mOnScrollListener" }; for (String listenerName : listenerNames) { Object listener = getListener(view, listenerName); if (listener != null && !mAllListenerHashcodes.contains(listener.hashCode())) { // print("has unhooked " + listenerName + ": " + view); return true; } } return false; } private Class<?> getClassByListenerName(String listenerName) { Class<?> viewClass = null; if ("mOnItemClickListener".equals(listenerName) || "mOnItemLongClickListener".equals(listenerName)) { viewClass = AdapterView.class; } else if ("mOnScrollListener".equals(listenerName)) { viewClass = AbsListView.class; } else if ("mOnChildClickListener".equals(listenerName) || "mOnGroupClickListener".equals(listenerName)) { viewClass = ExpandableListView.class; } else { viewClass = View.class; } return viewClass; } /** * Get listener from view. e.g. (OnClickListener) getListener(view, * "mOnClickListener"); means get click listener. Listener is a private * property of a view, that's why this function is written. * * @param view * target view * @param targetClass * the class which fieldName belong to * @param fieldName * target listener. e.g. mOnClickListener, mOnLongClickListener, * mOnTouchListener, mOnKeyListener * @return listener object; null means no listeners has been found */ private Object getListener(View view, Class<?> targetClass, String fieldName) { int level = countLevelFromViewToFather(view, targetClass); if (-1 == level) { return null; } try { if (!(view instanceof AdapterView) && Build.VERSION.SDK_INT > 14) {// API Level 14: Android 4.0 Object mListenerInfo = ReflectHelper.getField(view, targetClass.getName(), "mListenerInfo"); return null == mListenerInfo ? null : ReflectHelper.getField(mListenerInfo, null, fieldName); } else { return ReflectHelper.getField(view, targetClass.getName(), fieldName); } } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { // eat it } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } /** * find parent until parent is father or java.lang.Object(to the end) * * @param view * target view * @param father * target father * @return positive means level from father; -1 means not found */ private int countLevelFromViewToFather(View view, Class<?> father) { if (null == view) { return -1; } int level = 0; Class<?> originalClass = view.getClass(); // find its parent while (true) { if (originalClass.equals(Object.class)) { return -1; } else if (originalClass.equals(father)) { return level; } else { level++; originalClass = originalClass.getSuperclass(); } } } private Object getListener(View view, String listenerName) { return getListener(view, getClassByListenerName(listenerName), listenerName); } private void setListener(View view, String listenerName, Object value) { setListener(view, getClassByListenerName(listenerName), listenerName, value); } /** * This method is used to replace listener.setOnListener(). * listener.setOnListener() is probably overrided by application, so its * behavior can not be expected. * * @param view * @param targetClass * @param fieldName * @param value */ private void setListener(View view, Class<?> targetClass, String fieldName, Object value) { int level = countLevelFromViewToFather(view, targetClass); if (-1 == level) { return; } try { if (!(view instanceof AdapterView) && Build.VERSION.SDK_INT > 14) {// API // Level: // 14. // Android // 4.0 Object mListenerInfo = ReflectHelper.getField(view, targetClass.getName(), "mListenerInfo"); if (null == mListenerInfo) { return; } ReflectHelper.setField(mListenerInfo, null, fieldName, value); } else { ReflectHelper.setField(view, targetClass.getName(), fieldName, value); } } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { // eat it // e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * These try-catch can not be merged. We need try to hook listeners as many * as possible. */ private void setHookListenerOnView(View view) { // for thread safe if (null == view) { return; } /* * if (view instanceof WebView && DEBUG_WEBVIEW) { new * WebElementRecorder(this).handleWebView((WebView) view); } */ // handle list if (view instanceof AdapterView) { if (view instanceof ExpandableListView) { handleExpandableListView((ExpandableListView) view); } else if (!(view instanceof Spinner)) { handleOnItemClickListener((AdapterView<?>) view); } if (view instanceof AbsListView) { handleOnScrollListener((AbsListView) view); } // view.isLongClickable() handleOnItemLongClickListener((AdapterView<?>) view); // adapterView.setOnItemSelectedListener(listener); // MenuItem.OnMenuItemClickListener } if (view.isLongClickable()) { handleOnLongClickListener(view); } if (view instanceof EditText) { hookEditText((EditText) view); } else { // handleOnClickListener can not replace handleOnTouchListener // because reason below. // There are some views which have click listener and touch listener // but only use touch listener. handleOnClickListener(view); } handleOnTouchListener(view); } private void handleOnScrollListener(AbsListView absListView) { OnScrollListener onScrollListener = (OnScrollListener) getListener(absListView, "mOnScrollListener"); // has hooked listener if (onScrollListener != null && mAllListenerHashcodes.contains(onScrollListener.hashCode())) { return; } mAbsListViewStates.put(getViewID(absListView), new AbsListViewState()); if (null != onScrollListener) { hookOnScrollListener(absListView, onScrollListener); } else { printLog("set onScrollListener [" + absListView + "]"); OnScrollListener onScrollListenerHooked = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { setOnScrollStateChanged(view, scrollState); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { setOnScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } }; setListener(absListView, "mOnScrollListener", onScrollListenerHooked); } // save hashcode of hooked listener OnScrollListener onScrollListenerHooked = (OnScrollListener) getListener(absListView, "mOnScrollListener"); if (onScrollListenerHooked != null) { mAllListenerHashcodes.add(onScrollListenerHooked.hashCode()); } } private void setOnScrollStateChanged(AbsListView view, int scrollState) { AbsListViewState absListViewState = mAbsListViewStates.get(getViewID(view)); if (null == absListViewState) { printLog("null == absListViewState !!!"); return; } mCurrentScrollState = scrollState; if (OnScrollListener.SCROLL_STATE_IDLE == scrollState) { printLog("getLastVisiblePosition:" + view.getLastVisiblePosition()); printLog("totalItemCount:" + absListViewState.totalItemCount); if (view.getLastVisiblePosition() + 1 == absListViewState.totalItemCount) { mIsAbsListViewToTheEnd = true; } outputAScroll(view); } if (OnScrollListener.SCROLL_STATE_TOUCH_SCROLL == scrollState) { absListViewState.lastFirstVisibleItem = view.getFirstVisiblePosition(); } } private void setOnScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { AbsListViewState absListViewState = mAbsListViewStates.get(getViewID(view)); if (null == absListViewState) { printLog("null == absListViewState !!!"); return; } absListViewState.firstVisibleItem = firstVisibleItem; absListViewState.visibleItemCount = visibleItemCount; absListViewState.totalItemCount = totalItemCount; if (firstVisibleItem + visibleItemCount == totalItemCount && firstVisibleItem != 0) { // printLog("firstVisibleItem:" + firstVisibleItem); // printLog("visibleItemCount:" + visibleItemCount); // printLog("totalItemCount:" + totalItemCount); outputAScroll(view); } } private void outputAScroll(AbsListView view) { AbsListViewState absListViewState = mAbsListViewStates.get(getViewID(view)); if (null == absListViewState || absListViewState.totalItemCount == 0 || absListViewState.visibleItemCount == 0 || absListViewState.lastFirstVisibleItem == absListViewState.firstVisibleItem) { return; } printLog("mLastFirstVisibleItem:" + absListViewState.lastFirstVisibleItem); printLog("mFirstVisibleItem:" + absListViewState.firstVisibleItem); printLog("getFirstVisiblePosition:" + view.getFirstVisiblePosition()); absListViewState.lastFirstVisibleItem = absListViewState.firstVisibleItem; ScrollEvent scrollEvent = new ScrollEvent(view); scrollEvent.setCode(getPrefix(view) + "|scroll"); scrollEvent.setLog("scroll " + view + " to " + absListViewState.firstVisibleItem); offerOutputEventQueue(scrollEvent); } private void hookOnScrollListener(final AbsListView absListView, final OnScrollListener onScrollListener) { printLog("hook onScrollListener [" + absListView + "] [" + onScrollListener.hashCode() + "]"); // save old listener mOnScrollListeners.put(getViewID(absListView), onScrollListener); // mOnScrollListeners.put(String.valueOf(absListView.hashCode()), // onScrollListener); // set hook listener final OnScrollListener onScrollListenernew = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { setOnScrollStateChanged(view, scrollState); OnScrollListener onScrollListener = mOnScrollListeners.get(getViewID(view)); OnScrollListener onScrollListenerHooked = (OnScrollListener) getListener(view, "mOnScrollListener"); if (onScrollListener != null) { // TODO It's a bug. It can not be fix by below. if (onScrollListener.equals(onScrollListenerHooked)) { printLog("onScrollListenerHooked == onScrollListener!!!"); return; } onScrollListener.onScrollStateChanged(view, scrollState); } else { printLog("onScrollListener == null "); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { setOnScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); OnScrollListener onScrollListener = mOnScrollListeners.get(String.valueOf(view .hashCode())); OnScrollListener onScrollListenerHooked = (OnScrollListener) getListener(view, "mOnScrollListener"); if (onScrollListener != null) { // TODO It's a bug. It can not be fix by below. if (onScrollListener.equals(onScrollListenerHooked)) { printLog("onScrollListenerHooked == onScrollListener!!!"); return; } onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } else { printLog("onScrollListener == null "); } } }; absListView.post(new Runnable() { public void run() { absListView.setOnScrollListener(onScrollListenernew); } }); } private void handleExpandableListView(ExpandableListView expandableListView) { handleOnGroupClickListener(expandableListView); handleOnChildClickListener(expandableListView); } private void handleOnGroupClickListener(final ExpandableListView expandableListView) { OnGroupClickListener onGroupClickListener = (OnGroupClickListener) getListener( expandableListView, "mOnGroupClickListener"); // has hooked listener if (onGroupClickListener != null && mAllListenerHashcodes.contains(onGroupClickListener.hashCode())) { return; } if (null != onGroupClickListener) { hookOnGroupClickListener(expandableListView, onGroupClickListener); } else { printLog("set onGroupClickListener [" + expandableListView + "]"); OnGroupClickListener onGroupClickListenerHooked = new OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { setOnGroupClick(parent, groupPosition); return false; } }; setListener(expandableListView, "mOnGroupClickListener", onGroupClickListenerHooked); } // save hashcode of hooked listener OnGroupClickListener onGroupClickListenerHooked = (OnGroupClickListener) getListener( expandableListView, "mOnGroupClickListener"); if (onGroupClickListenerHooked != null) { mAllListenerHashcodes.add(onGroupClickListenerHooked.hashCode()); } } private void setOnGroupClick(ExpandableListView parent, int groupPosition) { int flatListPosition = parent.getFlatListPosition(ExpandableListView .getPackedPositionForGroup(groupPosition)); ClickEvent clickEvent = new ClickEvent(parent); String code = String.format(getPrefix(parent) + "|click"); clickEvent.setCode(code); clickEvent.setLog(String.format("click on group[%s]", groupPosition)); offerOutputEventQueue(clickEvent); } private void hookOnGroupClickListener(final ExpandableListView expandableListView, OnGroupClickListener onGroupClickListener) { printLog("hook onGroupCollapseListener [" + expandableListView + "]"); // save old listener mOnGroupClickListeners.put(getViewID(expandableListView), onGroupClickListener); // set hook listener expandableListView.setOnGroupClickListener(new OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { setOnGroupClick(parent, groupPosition); OnGroupClickListener onGroupClickListener = mOnGroupClickListeners .get(getViewID(expandableListView)); if (onGroupClickListener != null) { onGroupClickListener.onGroupClick(parent, v, groupPosition, id); } else { printLog("onGroupClickListener == null"); } return false; } }); } private void handleOnChildClickListener(final ExpandableListView expandableListView) { OnChildClickListener onChildClickListener = (OnChildClickListener) getListener( expandableListView, "mOnChildClickListener"); // has hooked listener if (onChildClickListener != null && mAllListenerHashcodes.contains(onChildClickListener.hashCode())) { return; } if (null != onChildClickListener) { hookOnChildClickListener(expandableListView, onChildClickListener); } else { printLog("set onChildClickListener [" + expandableListView + "]"); OnChildClickListener onChildClickListenerHooked = new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { setOnChildClick(expandableListView, groupPosition, childPosition); return false; } }; setListener(expandableListView, "mOnChildClickListener", onChildClickListenerHooked); } // save hashcode of hooked listener OnChildClickListener onChildClickListenerHooked = (OnChildClickListener) getListener( expandableListView, "mOnChildClickListener"); if (onChildClickListenerHooked != null) { mAllListenerHashcodes.add(onChildClickListenerHooked.hashCode()); } } private void setOnChildClick(ExpandableListView parent, int groupPosition, int childPosition) { int flatListPosition = parent.getFlatListPosition(ExpandableListView .getPackedPositionForChild(groupPosition, childPosition)); ClickEvent clickEvent = new ClickEvent(parent); String code = String.format(getPrefix(parent) + "|click"); clickEvent.setCode(code); clickEvent.setLog(String.format("click on group[%s] child[%s]", groupPosition, childPosition)); offerOutputEventQueue(clickEvent); } private void hookOnChildClickListener(final ExpandableListView expandableListView, OnChildClickListener onChildClickListener) { printLog("hook onChildClickListener [" + expandableListView + "]"); // save old listener mOnChildClickListeners.put(getViewID(expandableListView), onChildClickListener); // set hook listener expandableListView.setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { setOnChildClick(expandableListView, groupPosition, childPosition); OnChildClickListener onChildClickListener = mOnChildClickListeners .get(getViewID(expandableListView)); if (onChildClickListener != null) { onChildClickListener.onChildClick(parent, v, groupPosition, childPosition, id); } else { printLog("onChildClickListener == null"); } return false; } }); } private boolean handleOnClickListener(View view) { OnClickListener onClickListener = (OnClickListener) getListener(view, "mOnClickListener"); // has hooked listener if (onClickListener != null && mAllListenerHashcodes.contains(onClickListener.hashCode())) { return true; } if (onClickListener != null) { try { hookOnClickListener(view, onClickListener); } catch (Exception e) { e.printStackTrace(); } return true; } else { // only care of views which has OnClickListener } return false; } private void hookOnClickListener(final View view, final OnClickListener onClickListener) { // printLog(String.format("hookClickListener [%s(%s)]", view, local.getViewText(view))); // save old listener OnClickListener originListener = onClickListener; //should use originListener = kryo.copy(onClickListener); mOnClickListeners.put(getViewID(view), originListener); view.post(new Runnable() { @Override public void run() { // init counter mOnClickListenerInvokeCounter.put(onClickListener, 0); // set hook listener OnClickListener onClickListenerHooked = new OnClickListener() { @Override public void onClick(View v) { boolean shouldInvokeOrigin = false; int counter = mOnClickListenerInvokeCounter.get(onClickListener); mOnClickListenerInvokeCounter.put(onClickListener, ++counter); if (counter < 2) { setOnClick(v); } else { printLog("recover onClickListener counter:" + counter); setListener(view, "mOnClickListener", onClickListener); shouldInvokeOrigin = true; } if (shouldInvokeOrigin) { onClickListener.onClick(view); } // reset counter mOnClickListenerInvokeCounter.put(onClickListener, 0); } }; OnClickListener originOnClickListener = mOnClickListeners.get(getViewID(view)); if (onClickListenerHooked.equals(originOnClickListener)) { printLog("#########onClickListenerHooked.equals(originOnClickListener):" + onClickListenerHooked); } else { setListener(view, "mOnClickListener", onClickListenerHooked); } } }); // save hashcode of hooked listener OnClickListener onClickListenerHooked = (OnClickListener) getListener(view, "mOnClickListener"); if (onClickListenerHooked != null) { mAllListenerHashcodes.add(onClickListenerHooked.hashCode()); } } private void setOnClick(View v) { if (isSize0(v)) { printLog(v + " is size 0 "); invokeOriginOnClickListener(v); return; } // set click event output ClickEvent clickEvent = new ClickEvent(v); clickEvent.setCode(getPrefix(v) + "|click"); offerOutputEventQueue(clickEvent); invokeOriginOnClickListener(v); } private void invokeOriginOnClickListener(View v) { OnClickListener onClickListener = mOnClickListeners.get(getViewID(v)); OnClickListener onClickListenerHooked = (OnClickListener) getListener(v, "mOnClickListener"); if (onClickListener != null) { if (onClickListener.equals(onClickListenerHooked)) { printLog("onClickListener == onClickListenerHooked!!!"); return; } onClickListener.onClick(v); } else { printLog("onClickListener == null"); } } private String getRString(View view) { String rStringSuffix = getRStringSuffix(view); return "".equals(rStringSuffix) ? "" : "R.id." + rStringSuffix; } private String getRStringSuffix(View view) { int id = view.getId(); if (-1 == id) { return ""; } try { String rString = mContext.getResources().getResourceName(view.getId()); return rString.substring(rString.lastIndexOf("/") + 1, rString.length()); } catch (Exception e) { // eat it because some view has no res id } return ""; } private void hookEditText(final EditText editText) { if (mAllEditTexts.contains(editText)) { return; } // save origin text mEditTextLastText.put(getViewID(editText), editText.getText().toString()); // all TextWatchers work at the same time editText.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { String text = s.toString().replace("\\", "\\\\").replace("\"", "\\\"") .replace("\r\n", "\\n").replace("\n", "\\n"); String lastText = mEditTextLastText.get(getViewID(editText)); if ("".equals(s.toString()) || text.equals(lastText) || !editText.isShown() || !editText.isFocused()) { return; } printLog("onTextChanged: " + text + " getVisibility:" + editText + " " + editText.getVisibility()); mTheLastTextChangedTime = System.currentTimeMillis(); mCurrentEditTextIndex = getCurrentViewIndex(editText); mEditTextLastText.put(getViewID(editText), text); mCurrentEditTextString = text; mHasTextChange = true; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { } }); printLog("hookEditText [" + editText + "]"); mAllEditTexts.add(editText); } /** * get view index by its class at current activity * * @param view * @return -1 means not found;otherwise is then index of view */ private int getCurrentViewIndex(View view) { if (null == view) { return -1; } ArrayList<? extends View> views = removeInvisibleViews(getCurrentViews(view.getClass())); for (int i = 0; i < views.size(); i++) { if (views.get(i).equals(view)) { return i; } } return -1; } /** * Returns an {@code ArrayList} of {@code View}s of the specified * {@code Class} located in the current {@code Activity}. * * @param classToFilterBy * return all instances of this class, e.g. {@code Button.class} * or {@code GridView.class} * @return an {@code ArrayList} of {@code View}s of the specified * {@code Class} located in the current {@code Activity} */ private <T extends View> ArrayList<T> getCurrentViews(Class<T> classToFilterBy) { return getCurrentViews(classToFilterBy, null); } /** * Returns an {@code ArrayList} of {@code View}s of the specified * {@code Class} located under the specified {@code parent}. * * @param classToFilterBy * return all instances of this class, e.g. {@code Button.class} * or {@code GridView.class} * @param parent * the parent {@code View} for where to start the traversal * @return an {@code ArrayList} of {@code View}s of the specified * {@code Class} located under the specified {@code parent} */ private <T extends View> ArrayList<T> getCurrentViews(Class<T> classToFilterBy, View parent) { ArrayList<T> filteredViews = new ArrayList<T>(); List<View> allViews = getViews(parent, true); for (View view : allViews) { if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { filteredViews.add(classToFilterBy.cast(view)); } } allViews = null; return filteredViews; } private void handleOnTouchListener(View view) { OnTouchListener onTouchListener = (OnTouchListener) getListener(view, "mOnTouchListener"); // has hooked listener if (onTouchListener != null && mAllListenerHashcodes.contains(onTouchListener.hashCode())) { return; } if (null != onTouchListener) { hookOnTouchListener(view, onTouchListener); } else { // mOnClickListenerCounters.put(onTouchListener,0); // printLog("setOnTouchListener [" + view + "]"); OnTouchListener onTouchListenerHooked = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { addEvent(v, event); return false; } }; setListener(view, "mOnTouchListener", onTouchListenerHooked); } // save hashcode of hooked listener OnTouchListener onTouchListenerHooked = (OnTouchListener) getListener(view, "mOnTouchListener"); if (onTouchListenerHooked != null) { mAllListenerHashcodes.add(onTouchListenerHooked.hashCode()); } } private void hookOnTouchListener(View view, final OnTouchListener onTouchListener) { // printLog("hookOnTouchListener [" + view + "(" + local.getViewText(view) + ")]"); // save old listener mOnTouchListeners.put(getViewID(view), onTouchListener); // init counter mOnTouchListenerInvokeCounter.put(onTouchListener, 0); // set hook listener OnTouchListener onTouchListenerHooked = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { boolean ret = false; boolean shouldInvokeOrigin = false; int counter = mOnTouchListenerInvokeCounter.get(onTouchListener); mOnTouchListenerInvokeCounter.put(onTouchListener, ++counter); if (counter < 2) { OnTouchListener onTouchListenerHooked = (OnTouchListener) getListener(v, "mOnTouchListener"); addEvent(v, event); if (onTouchListener != null) { if (onTouchListener.equals(onTouchListenerHooked)) { printLog("onTouchListenerHooked == onTouchListener!!!"); return false; } ret = onTouchListener.onTouch(v, event); } else { printLog("onTouchListener == null"); } } else { printLog("recover onTouchListener counter:" + counter); setListener(v, "mOnTouchListener", onTouchListener); shouldInvokeOrigin = true; } if (shouldInvokeOrigin) { ret = onTouchListener.onTouch(v, event); } // reset counter mOnTouchListenerInvokeCounter.put(onTouchListener, 0); return ret; } }; setListener(view, "mOnTouchListener", onTouchListenerHooked); } private void addEvent(View v, MotionEvent event) { // printLog(v + " " + event); if (!offerMotionEventQueue(new RecordMotionEvent(v, event.getAction(), event.getRawX(), event.getRawY(), SystemClock.currentThreadTimeMillis()))) { printLog("Add to mMotionEventQueue Failed! view:" + v + "\t" + event.toString() + "mMotionEventQueue.size=" + mMotionEventQueue.size()); } } private void handleOnItemClickListener(AdapterView<?> adapterView) { OnItemClickListener onItemClickListener = (OnItemClickListener) getListener(adapterView, "mOnItemClickListener"); // has hooked listener if (onItemClickListener != null && mAllListenerHashcodes.contains(onItemClickListener.hashCode())) { return; } if (null != onItemClickListener) { printLog("hook AdapterView [" + adapterView + "] [" + onItemClickListener.hashCode() + "]" + mAllListenerHashcodes.contains(onItemClickListener.hashCode())); // save old listener mOnItemClickListeners.put(getViewID(adapterView), onItemClickListener); } else { printLog("set onItemClickListener at [" + adapterView + "]"); } OnItemClickListener onItemClickListenerHooked = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { setOnItemClick(parent, view, position, id); } }; setListener(adapterView, "mOnItemClickListener", onItemClickListenerHooked); // save hashcode of hooked listener onItemClickListenerHooked = (OnItemClickListener) getListener(adapterView, "mOnItemClickListener"); if (onItemClickListenerHooked != null) { mAllListenerHashcodes.add(onItemClickListenerHooked.hashCode()); printLog("save onItemClickListenerHooked " + onItemClickListenerHooked.hashCode()); printLog("mAllListenerHashcodes.contains " + mAllListenerHashcodes.contains(onItemClickListenerHooked.hashCode())); printLog("mAllListenerHashcodes.size():" + mAllListenerHashcodes.size()); } } /** * @param parent * @param view * @param position * it can not be used for mutiple columns listview * @param id */ private void setOnItemClick(AdapterView<?> parent, View view, int position, long id) { ClickEvent clickEvent = new ClickEvent(parent); clickEvent.setCode(getPrefix(parent) + "|click"); clickEvent.setLog("parent: " + parent + " view: " + view + " position: " + position + " click"); offerOutputEventQueue(clickEvent); OnItemClickListener onItemClickListener = mOnItemClickListeners.get(getViewID(parent)); OnItemClickListener onItemClickListenerHooked = (OnItemClickListener) getListener(parent, "mOnItemClickListener"); if (onItemClickListener != null) { // TODO It's a bug. It can not be fix by below. if (onItemClickListener.equals(onItemClickListenerHooked)) { printLog("onItemClickListener == onItemClickListenerHooked!!!"); return; } onItemClickListener.onItemClick(parent, view, position, id); } else { printLog("onItemClickListener == null"); // parent.performItemClick(view, position, id); } } private void handleOnItemLongClickListener(AdapterView<?> view) { // if (local.isSize0(view)) { // printLog(view + " is size 0 at handleOnItemLongClickListener"); // return; // } OnItemLongClickListener onItemLongClickListener = (OnItemLongClickListener) getListener( view, "mOnItemLongClickListener"); // has hooked listener if (onItemLongClickListener != null && mAllListenerHashcodes.contains(onItemLongClickListener.hashCode())) { return; } if (null != onItemLongClickListener) { printLog("hookOnItemLongClickListener [" + view + "(" + getViewText(view) + ")]"); // save old listener mOnItemLongClickListeners.put(getViewID(view), onItemLongClickListener); // set hook listener view.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { setOnLongClick(view); OnItemLongClickListener onItemLongClickListener = mOnItemLongClickListeners .get(getViewID(parent)); if (onItemLongClickListener != null) { return onItemLongClickListener.onItemLongClick(parent, view, position, id); } else { printLog("onItemLongClickListener == null"); } return false; } }); // save hashcode of hooked listener OnItemLongClickListener onItemLongClickListenerHooked = (OnItemLongClickListener) getListener( view, "mOnItemLongClickListener"); if (onItemLongClickListenerHooked != null) { mAllListenerHashcodes.add(onItemLongClickListenerHooked.hashCode()); } } else { printLog("setOnItemLongClickListener at " + view); view.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { setOnLongClick(view); return false; } }); } } private void handleOnLongClickListener(View view) { if (isSize0(view)) { printLog(view + " is size 0 "); invokeOriginOnLongClickListener(view); return; } OnLongClickListener onLongClickListener = (OnLongClickListener) getListener(view, "mOnLongClickListener"); // has hooked listener if (onLongClickListener != null && mAllListenerHashcodes.contains(onLongClickListener.hashCode())) { return; } if (null != onLongClickListener) { printLog("hookOnLongClickListener [" + view + "(" + getViewText(view) + ")]"); // save old listener mOnLongClickListeners.put(getViewID(view), onLongClickListener); // set hook listener view.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { setOnLongClick(v); invokeOriginOnLongClickListener(v); return false; } }); // save hashcode of hooked listener OnLongClickListener onLongClickListenerHooked = (OnLongClickListener) getListener(view, "mOnLongClickListener"); if (onLongClickListenerHooked != null) { mAllListenerHashcodes.add(onLongClickListenerHooked.hashCode()); } } else { printLog("setOnLongClickListener at " + view); view.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { setOnLongClick(v); return false; } }); } } private void invokeOriginOnLongClickListener(View v) { OnLongClickListener onLongClickListener = mOnLongClickListeners.get(getViewID(v)); if (onLongClickListener != null) { onLongClickListener.onLongClick(v); } else { printLog("onLongClickListener == null"); } } private void setOnLongClick(View v) { ClickEvent clickEvent = new ClickEvent(v); clickEvent.setCode(getPrefix(v) + "|longclick"); // clickEvent.setLog(); offerOutputEventQueue(clickEvent); mIsLongClick = true; } private void handleOutputEventQueue() { // merge event in 50ms by their priorities new Thread(new Runnable() { @Override public void run() { try { ArrayList<OutputEvent> events = new ArrayList<OutputEvent>(); while (true) { OutputEvent e = pollOutputEventQueue(); if (e != null) { events.add(e); if (e.view instanceof WebView || mDragWithoutUp) { sleep(1000); } else { sleep(400); } // get all event while ((e = pollOutputEventQueue()) != null) { events.add(e); } Collections.sort(events, new SortByPriority()); events = removeDuplicatePriority(events); Collections.sort(events, new SortByView()); outputEvents(events); events.clear(); mDragWithoutUp = false; } else { sleep(50); } } } catch (Exception e) { e.printStackTrace(); } } }, "handleOutputEventQueue").start(); } private ArrayList<OutputEvent> removeDuplicatePriority(ArrayList<OutputEvent> events) { if (events.size() < 2) { return events; } ArrayList<OutputEvent> newEvents = new ArrayList<OutputEvent>(); newEvents.add(events.get(0)); for (int i = 1; i < events.size(); i++) { OutputEvent left = events.get(i - 1); OutputEvent current = events.get(i); if (current.priority != left.priority) { newEvents.add(current); } } return newEvents; } private OutputEvent pollOutputEventQueue() { synchronized (mSyncOutputEventQueue) { return mOutputEventQueue.poll(); } } private boolean offerOutputEventQueue(OutputEvent e) { synchronized (mSyncOutputEventQueue) { mTheCurrentEventOutputime = System.currentTimeMillis(); return mOutputEventQueue.offer(e); } } private RecordMotionEvent pollMotionEventQueue() { synchronized (mSyncMotionEventQueue) { return mMotionEventQueue.poll(); } } private boolean offerMotionEventQueue(RecordMotionEvent e) { synchronized (mSyncMotionEventQueue) { return mMotionEventQueue.offer(e); } } /** * poll a output log from mOutputLogQueue * * synchronized by mSyncOutputLogQueue * * @return a line of output log */ public String pollOutputLogQueue() { synchronized (mSyncOutputLogQueue) { return mOutputLogQueue.poll(); } } private boolean offerOutputLogQueue(String line) { synchronized (mSyncOutputLogQueue) { return mOutputLogQueue.offer(line); } } private void outputEvents(ArrayList<OutputEvent> events) { for (OutputEvent outputEvent : filterByRelationship(filterByProity(events))) { outputAnEvent(outputEvent); } } /** * get the youngest event from family and ignore parent events */ private ArrayList<OutputEvent> filterByRelationship(ArrayList<OutputEvent> events) { ArrayList<OutputEvent> newEvents = new ArrayList<OutputEvent>(); // init eventsFlag int[] eventsFlag = new int[events.size()]; for (int i = 0; i < eventsFlag.length; i++) { eventsFlag[i] = 0; } for (int i = 0; i < events.size(); i++) { if (1 == eventsFlag[i]) { continue; } OutputEvent outputEvent = events.get(i); ArrayList<OutputEvent> eventFamily = getEventsByRelationship(events, outputEvent); // get the longest family string Collections.sort(eventFamily, new SortByFamilyString()); newEvents.add(eventFamily.get(0)); // mark event which have been handled for (int j = 0; j < events.size(); j++) { eventsFlag[j] = eventFamily.contains(events.get(j)) ? 1 : 0; } } return newEvents; } private ArrayList<OutputEvent> getEventsByRelationship(ArrayList<OutputEvent> events, OutputEvent targetOutputEvent) { ArrayList<OutputEvent> newEvents = new ArrayList<OutputEvent>(); for (OutputEvent outputEvent : events) { if (getRelationship(targetOutputEvent.view, outputEvent.view) != 0) { newEvents.add(outputEvent); } } return newEvents; } /** * ignore low proity event */ private ArrayList<OutputEvent> filterByProity(ArrayList<OutputEvent> events) { ArrayList<OutputEvent> newEvents = new ArrayList<OutputEvent>(); int maxIndex = events.size() - 1; for (int i = 0; i <= maxIndex;) { OutputEvent event = events.get(i); if (i == maxIndex) { newEvents.add(event); break; } // NOTICE: Assume that one action just generates two outputevents. OutputEvent nextEvent = events.get(i + 1); if (getRelationship(event.view, nextEvent.view) != 0) { i += 2; // printLog("" + event.proity + " " + nextEvent.proity); if (event.priority > nextEvent.priority) { // printLog("event.proity > nextEvent.proity"); newEvents.add(event); } else if (event.priority < nextEvent.priority) { // printLog("event.proity < nextEvent.proity"); newEvents.add(nextEvent); } else { printLog("event.proity == nextEvent.proity"); newEvents.add(event); newEvents.add(nextEvent); } } else { i = nextEvent.priority == event.priority ? i + 2 : i + 1; newEvents.add(event); } } return newEvents; } private int getRelationship(View v1, View v2) { String familyString1 = getFamilyString(v1); String familyString2 = getFamilyString(v2); if (familyString1.contains(familyString2)) { return -1;// -1 means v1 is a child of v2 } else if (familyString2.contains(familyString1)) { return 1;// 1 means v1 is a parent of v2 } else { return 0;// 0 means v1 has no relationship with v2 } } private void outputAnEvent(OutputEvent event) { if (mTheCurrentEventOutputime >= mTheLastTextChangedTime) { if (outputEditTextEvent()) { printCode(event.getCode()); } else { printCode(event.getCode()); } printLog(event.getLog()); } else { printCode(event.getCode()); printLog(event.getLog()); outputEditTextEvent(); } } private String getPrefix(View view) { String viewId = ""; try { viewId = getRString(view); viewId = viewId.equals("") ? getViewText(view) : viewId; // if view has no id and no text, viewId == "" } catch (Exception e) { e.printStackTrace(); } return String.format("%s|%s|%s", getPrefix(), getViewString(view), viewId); } private String getPrefix() { String time = ""; String activity = ""; try { time = mSimpleDateFormat.format(new Date()); // need permission GET_TASK //activity = mActivityManager.getRunningTasks(1).get(0).topActivity.getClassName(); } catch (Exception e) { e.printStackTrace(); } return String.format("%s", time); } private boolean outputEditTextEvent() { if ("".equals(mCurrentEditTextString) || mCurrentEditTextIndex < 0 || !mHasTextChange) { return false; } String code = String.format("local.enterText(%s, \"%s\", false);", mCurrentEditTextIndex, mCurrentEditTextString); printCode(code); // restore var mCurrentEditTextString = ""; mCurrentEditTextIndex = -1; mHasTextChange = false; return true; } private final static int TIMEOUT_NEXT_EVENT = 100; /** * check mMotionEventQueue and merge MotionEvent to drag */ private void handleRecordMotionEventQueue() { new Thread(new Runnable() { @Override public void run() { ArrayList<RecordMotionEvent> events = new ArrayList<RecordMotionEvent>(); while (true) { // find MotionEvent with ACTION_UP RecordMotionEvent e = null; boolean isUp = false; boolean isDown = false; long timeout = 0; while (true) { if ((e = pollMotionEventQueue()) != null) { events.add(e); if (MotionEvent.ACTION_UP == e.action || MotionEvent.ACTION_CANCEL == e.action) { isUp = true; isDown = false; break; } if (MotionEvent.ACTION_MOVE == e.action) { isDown = false; } if (MotionEvent.ACTION_DOWN == e.action) { isDown = true; timeout = System.currentTimeMillis() + TIMEOUT_NEXT_EVENT; } if (e.view instanceof ScrollView && "".equals(mFamilyStringBeforeScroll)) { mFamilyStringBeforeScroll = getFamilyString(e.view); } } if (isDown && System.currentTimeMillis() > timeout && mCurrentScrollState != OnScrollListener.SCROLL_STATE_FLING && mCurrentScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL && !mIsAbsListViewToTheEnd) { // events.get(0) is ACTION_DOWN if (!isParentScrollable(events.get(0).view)) { printLog("output a drag without up at " + events.get(0).view); mDragWithoutUp = true; mergeMotionEvents(events); events.clear(); isDown = false; } else { // printLog("ignore a drag without up"); } } sleep(10); } if (isUp) { // remove other views // View targetView = events.get(events.size() - 1).view; ArrayList<RecordMotionEvent> aTouch = new ArrayList<RecordMotionEvent>(); for (RecordMotionEvent recordMotionEvent : events) { // if (recordMotionEvent.view.equals(targetView)) { aTouch.add(recordMotionEvent); // } } mDragWithoutUp = false; mergeMotionEvents(aTouch); events.clear(); } sleep(50); } } }, "handleRecordMotionEventQueue").start(); } /** * Merge touch events from ACTION_DOWN to ACTION_UP. */ private void mergeMotionEvents(ArrayList<RecordMotionEvent> events) { RecordMotionEvent down = events.get(0); RecordMotionEvent up = events.get(events.size() - 1); DragEvent dragEvent = new DragEvent(up.view); if (up.view instanceof ScrollView) { outputAfterScrollStop((ScrollView) up.view, dragEvent); return; } int stepCount = events.size() - 2; stepCount = stepCount > MIN_STEP_COUNT ? stepCount : MIN_STEP_COUNT; long duration = up.time - down.time; /* * if (0 == duration) { printLog("ignore drag event of [" + up.view + * "] because 0 == duration"); printLog("x:" + up.x + " y:" + up.y); * return; } */ dragEvent.setLog(String.format( "Drag [%s<%s>] from (%s,%s) to (%s, %s) by duration %s step %s", up.view, getFamilyString(up.view), down.x, down.y, up.x, up.y, duration, stepCount)); dragEvent.setCode(getPrefix(up.view) + "|drag"); if (up.view instanceof AbsListView || mIsLongClick /* || (up.view instanceof WebView && DEBUG_WEBVIEW) */) { printLog("ignore drag event of [" + up.view + "]"); mIsLongClick = false; return; } // wait for other type event sleep(100); offerOutputEventQueue(dragEvent); } private float toPercentX(float x) { return x / getDisplayX(); } private float toPercentY(float y) { return y / getDisplayY(); } /** * This method will cost 100ms to judge whether scrollview stoped. * * @param scrollView * @return true means scrolling is stop, otherwise return fasle */ private boolean isScrollStoped(final ScrollView scrollView) { int x1 = scrollView.getScrollX(); int y1 = scrollView.getScrollY(); sleep(100); int x2 = scrollView.getScrollX(); int y2 = scrollView.getScrollY(); return x1 == x2 && y1 == y2 ? true : false; } /** * Start a thread to wait for scroll stoping, and return immediately. * * @param scrollView * @param dragEvent */ private void outputAfterScrollStop(final ScrollView scrollView, final DragEvent dragEvent) { new Thread(new Runnable() { @Override public void run() { while (!isScrollStoped(scrollView)) { // wait for scroll stoping } if ("".equals(mFamilyStringBeforeScroll)) { printLog("mFamilyStringBeforeScroll is \"\""); return; } int scrollX = scrollView.getScrollX(); int scrollY = scrollView.getScrollY(); String drag = String.format( "local.recordReplay.scrollScrollViewTo(\"%s\", %s, %s);", mFamilyStringBeforeScroll, scrollX, scrollY); mFamilyStringBeforeScroll = ""; dragEvent.setLog(String.format("Scroll [%s] to (%s, %s)", scrollView, scrollX, scrollY)); dragEvent.setCode(drag); outputAnEvent(dragEvent); } }, "outputAfterScrollStop").start(); } private void handleOnKeyListener(View view) { // for thread safe if (null == view) { return; } OnKeyListener onKeyListener = (OnKeyListener) getListener(view, "mOnKeyListener"); // has hooked listener if (onKeyListener != null && mAllListenerHashcodes.contains(onKeyListener.hashCode())) { return; } if (null != onKeyListener) { hookOnKeyListener(view, onKeyListener); } else { // printLog("setOnKeyListener [" + view + "]"); view.setOnKeyListener(new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { setOnKey(v, keyCode, event); return false; } }); } // save hashcode of hooked listener OnKeyListener onKeyListenerHooked = (OnKeyListener) getListener(view, "mOnKeyListener"); if (onKeyListenerHooked != null) { mAllListenerHashcodes.add(onKeyListenerHooked.hashCode()); } } private void hookOnKeyListener(View view, OnKeyListener onKeyListener) { printLog("hookOnKeyListener [" + view + "]"); mOnKeyListeners.put(getViewID(view), onKeyListener); view.setOnKeyListener(new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { setOnKey(v, keyCode, event); OnKeyListener onKeyListener = mOnKeyListeners.get(getViewID(v)); if (null != onKeyListener) { onKeyListener.onKey(v, keyCode, event); } else { printLog("onKeyListener == null"); } return false; } }); } private void setOnKey(View view, int keyCode, KeyEvent event) { // ignore KeyEvent.ACTION_DOWN if (event.getAction() == KeyEvent.ACTION_UP) { if (view instanceof EditText && keyCode != KeyEvent.KEYCODE_MENU && keyCode != KeyEvent.KEYCODE_BACK) { return; } HardKeyEvent hardKeyEvent = new HardKeyEvent(view); hardKeyEvent.setCode(getPrefix() + "|key|" + mKeyCodeMap.get(keyCode)); hardKeyEvent.setLog("view: " + view + " " + event); offerOutputEventQueue(hardKeyEvent); } } /** * for view.getId() == -1 */ private String getViewID(View view) { if (null == view) { printLog("null == view "); return ""; } try { String viewString = view.toString(); if (viewString.indexOf('@') != -1) { return viewString.substring(viewString.indexOf("@")); } else if (viewString.indexOf('{') != -1) { // after android 4.2 int leftBracket = viewString.indexOf('{'); int firstSpace = viewString.indexOf(' '); return viewString.substring(leftBracket + 1, firstSpace); } else { return viewString + view.getId(); } } catch (Exception e) { // TODO: handle exception return String.valueOf(view.getId()); } } private String getViewString(View view) { return view.getClass().toString().split(" ")[1]; } private void initKeyTable() { KeyEvent keyEvent = new KeyEvent(0, 0); ArrayList<String> names = ReflectHelper.getFieldNameByType(keyEvent, null, int.class); try { for (String name : names) { if (name.startsWith("KEYCODE_")) { Integer keyCode = (Integer) ReflectHelper.getField(keyEvent, null, name); mKeyCodeMap.put(keyCode, name); } } } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } private boolean isParentScrollable(View view) { while (view.getParent() instanceof ViewGroup) { ViewGroup parent = (ViewGroup) view.getParent(); if (parent instanceof ScrollView || parent instanceof AbsListView) { return true; } view = parent; } return null == view.getParent() ? false : true; } }