// 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.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; 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. *

*/ String activityName; /** *

* Represents the operations to the log file. *

*/ private AndroidMonitorLogFile logFile; /** * 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) { activityName = activity.getClass().getSimpleName(); logFile = new AndroidMonitorLogFile(getAppLable(activity), activity.getFilesDir()); addLogListenerToView(getRootView(activity)); // TODO // listen to changes and update own listener // find out if it is possible to directly call addLogListenerToView // again // activity.onContentChanged(); // write backPresss as event to xml file // activity.onBackPressed(); // handle onStop() method of the activity // add a function that end up tracking if onStop() is given otherwise // create onStop() // http://developer.android.com/training/basics/activity-lifecycle/stopping.html } /** * get the root view of an activity * * @param activity * @return */ public 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) { 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. * * @param view * to be monitored */ public 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 (!logFile.isComponentLogged(view.hashCode())) { logFile.addComponent(view, parentHash, activityName); } // hash code of the actual view element. Problem in using // view.getParent().hashCode() is described in // de.ugoe.cs.autoquest.androidmonitor#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); } } // 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) { logFile.addEvent(v, "onClick"); } }); 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) { logFile.addEvent(hashOfView, "text", s.toString()); } }); } } /** * finds out if a listener exists * * @param view * @return the listener of the view or null if no listener exists */ public View.OnClickListener getOnClickListener(View view) { // http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views 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) 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) 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; } /** * get application name as defined in Package Name * * @param pContext * package context; could also be an activity * @return app name */ public String getAppLable(Context pContext) { // source (2014-09-04): // http://stackoverflow.com/questions/11229219/android-get-application-name-not-package-name PackageManager lPackageManager = pContext.getPackageManager(); ApplicationInfo lApplicationInfo = null; try { lApplicationInfo = lPackageManager.getApplicationInfo(pContext.getApplicationInfo().packageName, 0); } catch (final NameNotFoundException e) {} return (String) (lApplicationInfo != null ? lPackageManager .getApplicationLabel(lApplicationInfo) : "Unknown"); } }