// Copyright 2012 Georg-August-Universität Göttingen, Germany // // 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 de.ugoe.cs.autoquest.androidmonitor; import java.lang.reflect.Field; import de.ugoe.cs.autoquest.androidmonitor.AndroidMonitorCompositeOnClickListener; import android.app.Activity; import android.os.Build; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; /** *

* Monitor an android activity. Call this class via * AndroidMonitor.getInstanceOfAndroidMonitor().startMonitor(this); at the end of the * onCreate(Bundle savedInstanceState) method. *

* * @author Florian Unger * @version 1.0 */ public class AndroidMonitor { /** *

* Name of the Activity that starts the monitor. *

*/ private String activityName; /** *

* Represents the operations to the log file. *

*/ private static AndroidMonitorLogFile logFile = null; /** *

* Represents the position of the current view element in the tree structure of an activity. *

*/ private int elementPosition = 0; /** * Constructor method to get a monitor object. * * @return monitor */ public static AndroidMonitor getInstanceOfAndroidMonitor() { AndroidMonitor monitor = new AndroidMonitor(); return monitor; } /** * starts tracking an activity * * @param activity */ public void startMonitor(Activity activity) { // create logFile only once. if (AndroidMonitor.logFile == null) { AndroidMonitor.logFile = new AndroidMonitorLogFile(activity); Log.i("logfile", "file created"); } activityName = activity.getClass().getSimpleName(); addLogListenerToView(getRootView(activity)); /* * TODO listen to changes and update own listener. - Listen to known elements is implemented * and trigger startMonitot(View). */ /* * TODO write backPresss as event to xml file activity.onBackPressed(); */ /* * TODO handle onStop() method of the activity add a function that end up tracking if * onStop() is given otherwise. Maybe: create onStop() * http://developer.android.com/training/basics/activity-lifecycle/stopping.html */ } public void startMonitor(View view) { addLogListenerToView(findFirstView(view)); } /** * Get the root view of an activity. * * @param activity * @return */ private View getRootView(Activity activity) { // get root view of the activity as start point View view = activity.getWindow().getDecorView().getRootView(); // try out if the given node is the upper one in the tree and return the // first node of the tree // The root of the decorView could be embedded into another layout // element. return findFirstView(view); } /** * Returns first view element of the tree. * * @param view * @return */ private View findFirstView(View view) { elementPosition = 0; if (view.getParent() != null && (view.getParent() instanceof ViewGroup)) { return findFirstView((View) view.getParent()); } else { return view; } } /** * Replace the listener of each view with a composite listener which collects several listeners * for one view. Add a TextWatcher to all TexView views. * * @param view * to be monitored */ private void addLogListenerToView(View view) { addLogListenerToView(view, 0); } /** * Replace the listener of each view with a composite listener which collects several listeners * for one view. * * @param view * to be monitored * @param parentHash * hash of the parent view element */ private void addLogListenerToView(View view, int parentHash) { if (!AndroidMonitorLogFile.isComponentLogged(view.hashCode())) { AndroidMonitor.logFile.addComponent(view, parentHash, activityName, getElementPosition()); // Log.i("object type", ""); // save original listener to add it later on to the groupLisatener View.OnClickListener onClickListener = getOnClickListener(view); if (onClickListener != null) { // create new compositeOnClickListener to handle multiple listeners // for one view AndroidMonitorCompositeOnClickListener groupListener = new AndroidMonitorCompositeOnClickListener(); // replace the original onClickListener with the // compositeOnClickListener view.setOnClickListener(groupListener); // add the tracking part as a several listener groupListener.addOnClickListener(new View.OnClickListener() { public void onClick(View v) { AndroidMonitor.logFile.addEvent(v, "onClick"); // check if something changed in the activity startMonitor(v); } }); groupListener.addOnClickListener(onClickListener); } // if view is a TextView add a addTextChangedListener to this view if (view instanceof TextView) { final int hashOfView = view.hashCode(); TextView textView = (TextView) view; textView.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { // do nothing } public void onTextChanged(CharSequence s, int start, int before, int count) { // do nothing } public void afterTextChanged(Editable s) { AndroidMonitor.logFile.addEvent(hashOfView, "text", s.toString()); } }); } } // hash code of the actual view element. Problem in using // view.getParent().hashCode() is described in // de.ugoe.cs.autoquest.androidmonitor.AndroidMonitorLogFile#addComponent() parentHash = view.hashCode(); // traverse all views of the activity if (view instanceof ViewGroup) { ViewGroup group = (ViewGroup) view; for (int i = 0; i < group.getChildCount(); i++) { View child = group.getChildAt(i); addLogListenerToView(child, parentHash); } } } /** * finds out if a listener exists * * @param view * @return the listener of the view or null if no listener exists */ // source: http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views private View.OnClickListener getOnClickListener(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return getOnClickListenerV14(view); } else { return getOnClickListenerV(view); } } // Used for APIs lower than ICS (API 14) // source: http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views private View.OnClickListener getOnClickListenerV(View view) { View.OnClickListener retrievedListener = null; String viewStr = "android.view.View"; Field field; try { field = Class.forName(viewStr).getDeclaredField("mOnClickListener"); retrievedListener = (View.OnClickListener) field.get(view); } catch (NoSuchFieldException ex) { Log.e("Reflection", "No Such Field."); } catch (IllegalAccessException ex) { Log.e("Reflection", "Illegal Access."); } catch (ClassNotFoundException ex) { Log.e("Reflection", "Class Not Found."); } return retrievedListener; } // Used for new ListenerInfo class structure used beginning with API 14 // (ICS) // source: http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views private View.OnClickListener getOnClickListenerV14(View view) { View.OnClickListener retrievedListener = null; String viewStr = "android.view.View"; String lInfoStr = "android.view.View$ListenerInfo"; try { Field listenerField = Class.forName(viewStr).getDeclaredField("mListenerInfo"); Object listenerInfo = null; if (listenerField != null) { listenerField.setAccessible(true); listenerInfo = listenerField.get(view); } Field clickListenerField = Class.forName(lInfoStr).getDeclaredField("mOnClickListener"); if (clickListenerField != null && listenerInfo != null) { retrievedListener = (View.OnClickListener) clickListenerField.get(listenerInfo); } } catch (NoSuchFieldException ex) { Log.e("Reflection", "No Such Field."); } catch (IllegalAccessException ex) { Log.e("Reflection", "Illegal Access."); } catch (ClassNotFoundException ex) { Log.e("Reflection", "Class Not Found."); } return retrievedListener; } /** * *

* Return the AndroidMonitorLogFile that is used in the AndroidMonitor. *

* * @return AndroidMonitorLogFile that is used in the AndroidMonitor. */ public AndroidMonitorLogFile getLogFile() { return logFile; } private int getElementPosition(){ int element = elementPosition; elementPosition ++; return element; } }