| diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java |
| index ecd0050..5053c79 100644 |
| --- a/core/java/android/app/Activity.java |
| +++ b/core/java/android/app/Activity.java |
| @@ -84,6 +84,7 @@ import android.view.ActionMode; |
| import android.view.ContextMenu; |
| import android.view.ContextMenu.ContextMenuInfo; |
| import android.view.ContextThemeWrapper; |
| +import android.view.GestureDetector; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| @@ -104,6 +105,11 @@ import android.view.WindowManagerGlobal; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.AdapterView; |
| |
| +import org.json.JSONArray; |
| +import org.json.JSONException; |
| +import org.json.JSONObject; |
| +import org.json.JSONStringer; |
| + |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.lang.annotation.Retention; |
| @@ -700,6 +706,74 @@ public class Activity extends ContextThemeWrapper |
| } |
| private SparseArray<ManagedDialog> mManagedDialogs; |
| |
| + /* RICO: Logs gestures on activities through logcat. */ |
| + private GestureDetector mRicoGestureDetector; |
| + private GestureLogger mRicoGestureLogger; |
| + |
| + private static class GestureLogger implements GestureDetector.OnGestureListener, |
| + GestureDetector.OnDoubleTapListener { |
| + private String activityName; |
| + private static final String DEBUG_TAG = "RICO_Gestures"; |
| + |
| + public GestureLogger(String activityName) { |
| + this.activityName = activityName; |
| + } |
| + |
| + @Override |
| + public boolean onDown(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "down:" + activityName); |
| + return true; |
| + } |
| + |
| + @Override |
| + public boolean onFling(MotionEvent event1, MotionEvent event2, |
| + float velocityX, float velocityY) { |
| + Log.d(DEBUG_TAG, "fling:" + activityName); |
| + return true; |
| + } |
| + |
| + @Override |
| + public void onLongPress(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "long_press:" + activityName); |
| + } |
| + |
| + @Override |
| + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, |
| + float distanceY) { |
| + Log.d(DEBUG_TAG, "scroll:" + activityName); |
| + return true; |
| + } |
| + |
| + @Override |
| + public void onShowPress(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "show_press:" + activityName); |
| + } |
| + |
| + @Override |
| + public boolean onSingleTapUp(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "single_tap_up:" + activityName); |
| + return true; |
| + } |
| + |
| + @Override |
| + public boolean onDoubleTap(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "double_tap:" + activityName); |
| + return true; |
| + } |
| + |
| + @Override |
| + public boolean onDoubleTapEvent(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "double_tap_event:" + activityName); |
| + return true; |
| + } |
| + |
| + @Override |
| + public boolean onSingleTapConfirmed(MotionEvent event) { |
| + Log.d(DEBUG_TAG, "single_tap_confirmed:" + activityName); |
| + return true; |
| + } |
| + } |
| + |
| // set by the thread after the constructor and before onCreate(Bundle savedInstanceState) is called. |
| private Instrumentation mInstrumentation; |
| private IBinder mToken; |
| @@ -924,6 +998,13 @@ public class Activity extends ContextThemeWrapper |
| mVoiceInteractor.attachActivity(this); |
| } |
| mCalled = true; |
| + |
| + /* |
| + * RICO: Initializes the gesture logger. |
| + */ |
| + mRicoGestureLogger = new GestureLogger(this.getComponentName().flattenToString()); |
| + mRicoGestureDetector = new GestureDetector(this, mRicoGestureLogger); |
| + mRicoGestureDetector.setOnDoubleTapListener(mRicoGestureLogger); |
| } |
| |
| /** |
| @@ -5560,6 +5641,17 @@ public class Activity extends ContextThemeWrapper |
| } |
| |
| /** |
| + * RICO: Used to access FragmentManager instance to obtain active and |
| + * added fragment names. dumpActiveAndAddedFragments() is a custom method |
| + * in the FragmentManager class that we added. |
| + * @hide |
| + */ |
| + public void dumpFragments(JSONObject view) throws JSONException { |
| + FragmentManager fragManager = mFragments.getFragmentManager(); |
| + fragManager.dumpActiveAndAddedFragments(view); |
| + } |
| + |
| + /** |
| * Print the Activity's state into the given stream. This gets invoked if |
| * you run "adb shell dumpsys activity <activity_component_name>". |
| * |
| diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java |
| index fd88a05..94c4c00 100644 |
| --- a/core/java/android/app/ActivityThread.java |
| +++ b/core/java/android/app/ActivityThread.java |
| @@ -110,6 +110,11 @@ import com.android.org.conscrypt.OpenSSLSocketImpl; |
| import com.android.org.conscrypt.TrustedCertificateStore; |
| import com.google.android.collect.Lists; |
| |
| +import org.json.JSONArray; |
| +import org.json.JSONException; |
| +import org.json.JSONObject; |
| +import org.json.JSONStringer; |
| + |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileOutputStream; |
| @@ -942,6 +947,11 @@ public final class ActivityThread { |
| sendMessage(H.SCHEDULE_CRASH, msg); |
| } |
| |
| + /* |
| + * RICO: This method sends an asynchronous message that is put onto a queue. |
| + * The ActivityThread class later will handle the message with the method |
| + * handleMessage(). |
| + */ |
| public void dumpActivity(FileDescriptor fd, IBinder activitytoken, |
| String prefix, String[] args) { |
| DumpComponentInfo data = new DumpComponentInfo(); |
| @@ -950,7 +960,24 @@ public final class ActivityThread { |
| data.token = activitytoken; |
| data.prefix = prefix; |
| data.args = args; |
| - sendMessage(H.DUMP_ACTIVITY, data, 0, 0, true /*async*/); |
| + boolean stream = false; |
| + |
| + for (String arg: args) { |
| + if ("stream".equals(arg)) { |
| + stream = true; |
| + break; |
| + } |
| + } |
| + /* |
| + * If the stream argument is used when calling dumpActivity, |
| + * DUMP_ACTIVITY_TO_STREAM code will be sent. This results in the |
| + * invocation of a custom method (handleDumpActivityUIStream()) |
| + */ |
| + if (stream) { |
| + sendMessage(H.DUMP_ACTIVITY_TO_STREAM, data, 0, 0, true /*async*/); |
| + } else { |
| + sendMessage(H.DUMP_ACTIVITY, data, 0, 0, true /*async*/); |
| + } |
| } catch (IOException e) { |
| Slog.w(TAG, "dumpActivity failed", e); |
| } |
| @@ -1275,6 +1302,7 @@ public final class ActivityThread { |
| public static final int CANCEL_VISIBLE_BEHIND = 147; |
| public static final int BACKGROUND_VISIBLE_BEHIND_CHANGED = 148; |
| public static final int ENTER_ANIMATION_COMPLETE = 149; |
| + public static final int DUMP_ACTIVITY_TO_STREAM = 150; |
| |
| String codeToString(int code) { |
| if (DEBUG_MESSAGES) { |
| @@ -1328,6 +1356,7 @@ public final class ActivityThread { |
| case CANCEL_VISIBLE_BEHIND: return "CANCEL_VISIBLE_BEHIND"; |
| case BACKGROUND_VISIBLE_BEHIND_CHANGED: return "BACKGROUND_VISIBLE_BEHIND_CHANGED"; |
| case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE"; |
| + case DUMP_ACTIVITY_TO_STREAM: return "DUMP_ACTIVITY_TO_STREAM"; |
| } |
| } |
| return Integer.toString(code); |
| @@ -1557,6 +1586,9 @@ public final class ActivityThread { |
| case ENTER_ANIMATION_COMPLETE: |
| handleEnterAnimationComplete((IBinder) msg.obj); |
| break; |
| + case DUMP_ACTIVITY_TO_STREAM: |
| + handleDumpActivityToStream((DumpComponentInfo) msg.obj); |
| + break; |
| } |
| if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what)); |
| } |
| @@ -2981,6 +3013,47 @@ public final class ActivityThread { |
| } |
| } |
| |
| + /* |
| + * RICO: When the ActivityThread asynchronously receives a command to dump |
| + * activity with the argument stream, this method will be called. |
| + * The current top window's UI hierarchy will be dumped to the file descriptor |
| + * contained in the arguments. The current root view of |
| + * the top window is obtained by calling the modified getCurrentFocusRoot() |
| + * method of the WindowManagerGlobal class. |
| + */ |
| + private void handleDumpActivityToStream(DumpComponentInfo info) { |
| + final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); |
| + try { |
| + ActivityClientRecord r = mActivities.get(info.token); |
| + JSONObject view = new JSONObject(); |
| + if (r != null && r.activity != null) { |
| + r.activity.dumpFragments(view); |
| + |
| + WindowManagerGlobal wmg = WindowManagerGlobal.getInstance(); |
| + view.put("is_keyboard_deployed", wmg.isKeyboardDeployed()); |
| + |
| + ViewRootImpl root = wmg.getCurrentFocusRoot(); |
| + if (root == null) { |
| + view.put("is_external", true); |
| + } else { |
| + root.dumpRoot(view); |
| + } |
| + |
| + PrintWriter pw = new FastPrintWriter(new FileOutputStream( |
| + info.fd.getFileDescriptor())); |
| + pw.print("\"activity\": "); |
| + pw.print(view.toString()); |
| + pw.flush(); |
| + } |
| + } catch (JSONException e) { |
| + Log.e("RICO", "Exception in serializing View object to JSON."); |
| + Log.e("RICO", e.getMessage()); |
| + } finally { |
| + IoUtils.closeQuietly(info.fd); |
| + StrictMode.setThreadPolicy(oldPolicy); |
| + } |
| + } |
| + |
| private void handleDumpProvider(DumpComponentInfo info) { |
| final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); |
| try { |
| diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java |
| index 696ccdb..cd17a45 100644 |
| --- a/core/java/android/app/FragmentManager.java |
| +++ b/core/java/android/app/FragmentManager.java |
| @@ -45,6 +45,11 @@ import android.view.View; |
| import android.view.ViewGroup; |
| import com.android.internal.util.FastPrintWriter; |
| |
| +import org.json.JSONArray; |
| +import org.json.JSONException; |
| +import org.json.JSONObject; |
| +import org.json.JSONStringer; |
| + |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| @@ -333,6 +338,13 @@ public abstract class FragmentManager { |
| public abstract boolean isDestroyed(); |
| |
| /** |
| + * RICO: Print the FragmentManager's state into the given stream. |
| + * |
| + * @hide |
| + */ |
| + public abstract void dumpActiveAndAddedFragments(JSONObject view) throws JSONException; |
| + |
| + /** |
| * Print the FragmentManager's state into the given stream. |
| * |
| * @param prefix Text to print at the front of each line. |
| @@ -701,6 +713,35 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate |
| return sb.toString(); |
| } |
| |
| + /* |
| + * RICO: Adds the names of fragments from a list to the JSONObject for the view. |
| + */ |
| + private void dumpFragments(JSONObject view, ArrayList<Fragment> fragmentList, |
| + String name) throws JSONException { |
| + JSONArray fragments = new JSONArray(); |
| + if (fragmentList != null) { |
| + int numFragments = fragmentList.size(); |
| + for (int i = 0; i < numFragments; i++) { |
| + Fragment frag = fragmentList.get(i); |
| + if (frag == null) { |
| + continue; |
| + } |
| + int fragmentId = frag.getId(); |
| + if (fragmentId != 0) { |
| + fragments.put(Integer.toHexString(fragmentId)); |
| + } |
| + } |
| + } |
| + view.put(name, fragments); |
| + } |
| + |
| + |
| + public void dumpActiveAndAddedFragments(JSONObject view) throws JSONException { |
| + dumpFragments(view, mActive, "active_fragments"); |
| + dumpFragments(view, mAdded, "added_fragments"); |
| + } |
| + |
| + |
| @Override |
| public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { |
| String innerPrefix = prefix + " "; |
| diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl |
| index 5994d4f..bd6127d 100644 |
| --- a/core/java/android/view/IWindowManager.aidl |
| +++ b/core/java/android/view/IWindowManager.aidl |
| @@ -271,4 +271,18 @@ interface IWindowManager |
| * @return The frame statistics or null if the window does not exist. |
| */ |
| WindowContentFrameStats getWindowContentFrameStats(IBinder token); |
| + |
| + /** |
| + * Gets the flag associated with a given window. |
| + * |
| + * @return The flag of the currently focused window or -1 if the window does not exist. |
| + */ |
| + int getCurrentFlag(); |
| + |
| + /** |
| + * Gets state of the keyboard. |
| + * |
| + * @return True if the keyboard is deployed, false otherwise. |
| + */ |
| + boolean isKeyboardDeployed(); |
| } |
| diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java |
| index 720d9a8..6a1c499 100644 |
| --- a/core/java/android/view/View.java |
| +++ b/core/java/android/view/View.java |
| @@ -16,6 +16,7 @@ |
| |
| package android.view; |
| |
| +import android.app.Activity; |
| import android.animation.AnimatorInflater; |
| import android.animation.StateListAnimator; |
| import android.annotation.CallSuper; |
| @@ -95,8 +96,10 @@ import android.view.animation.Transformation; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.InputMethodManager; |
| +import android.widget.AdapterView; |
| import android.widget.Checkable; |
| import android.widget.FrameLayout; |
| +import android.widget.LinearLayout; |
| import android.widget.ScrollBarDrawable; |
| |
| import static android.os.Build.VERSION_CODES.*; |
| @@ -108,6 +111,11 @@ import com.android.internal.view.menu.MenuBuilder; |
| import com.google.android.collect.Lists; |
| import com.google.android.collect.Maps; |
| |
| +import org.json.JSONArray; |
| +import org.json.JSONException; |
| +import org.json.JSONObject; |
| +import org.json.JSONStringer; |
| + |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.WeakReference; |
| @@ -4554,6 +4562,115 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| mAttributes = trimmed; |
| } |
| |
| + /** |
| + * RICO: Gets all ancestor classes of the current object. |
| + * @hide |
| + */ |
| + private ArrayList<String> getAncestors() { |
| + ArrayList<String> retVal = new ArrayList<String>(); |
| + Class curClass = this.getClass(); |
| + Class superclass = curClass.getSuperclass(); |
| + while (superclass != null) { |
| + retVal.add(superclass.getName()); |
| + curClass = superclass; |
| + superclass = curClass.getSuperclass(); |
| + } |
| + return retVal; |
| + |
| + } |
| + |
| + /** |
| + * RICO: Arranges several properties of the View instance as a JSONObject. |
| + * @hide |
| + */ |
| + public JSONObject toJson() throws JSONException { |
| + JSONObject obj = new JSONObject(); |
| + obj.put("class", getClass().getName()); |
| + switch (mViewFlags & VISIBILITY_MASK) { |
| + case VISIBLE: |
| + obj.put("visibility", "visible"); |
| + break; |
| + case INVISIBLE: |
| + obj.put("visibility", "invisible"); |
| + break; |
| + case GONE: |
| + obj.put("visibility", "gone"); |
| + break; |
| + default: |
| + break; |
| + } |
| + obj.put("visible-to-user", this.isVisibleToUser()); |
| + obj.put("adapter-view", (this instanceof AdapterView)); |
| + obj.put("focusable", (mViewFlags & FOCUSABLE_MASK) == FOCUSABLE); |
| + obj.put("enabled", (mViewFlags & ENABLED_MASK) == ENABLED); |
| + obj.put("draw", (mViewFlags & DRAW_MASK) == WILL_NOT_DRAW); |
| + obj.put("scrollable-horizontal", (mViewFlags & SCROLLBARS_HORIZONTAL) != 0); |
| + obj.put("scrollable-vertical", (mViewFlags & SCROLLBARS_VERTICAL) != 0); |
| + obj.put("clickable", (mViewFlags & CLICKABLE) != 0 ); |
| + JSONArray ancestors = new JSONArray(); |
| + for (String className : getAncestors()) { |
| + ancestors.put(className); |
| + } |
| + obj.put("ancestors", ancestors ); |
| + obj.put("pointer", Integer.toHexString(System.identityHashCode(this))); |
| + obj.put("long-clickable", (mViewFlags & LONG_CLICKABLE) != 0); |
| + obj.put("focused", (mPrivateFlags & PFLAG_FOCUSED) != 0); |
| + obj.put("selected", (mPrivateFlags & PFLAG_SELECTED) != 0); |
| + if ((mPrivateFlags & PFLAG_PREPRESSED) == 0) { |
| + obj.put("pressed", (mPrivateFlags & PFLAG_PRESSED) != 0 ? "pressed" : "not_pressed"); |
| + } else { |
| + obj.put("pressed", "prepressed"); |
| + } |
| + JSONArray jsonRelBounds = new JSONArray(); |
| + jsonRelBounds.put(mLeft); |
| + jsonRelBounds.put(mTop); |
| + jsonRelBounds.put(mRight); |
| + jsonRelBounds.put(mBottom); |
| + if (mAttachInfo == null) { |
| + obj.put("abs-pos", false); |
| + obj.put("bounds", jsonRelBounds); |
| + } else { |
| + obj.put("abs-pos", true); |
| + Rect bounds = mAttachInfo.mTmpInvalRect; |
| + getBoundsOnScreen(bounds, true); |
| + JSONArray jsonBounds = new JSONArray(); |
| + jsonBounds.put(bounds.left); |
| + jsonBounds.put(bounds.top); |
| + jsonBounds.put(bounds.right); |
| + jsonBounds.put(bounds.bottom); |
| + obj.put("bounds", jsonBounds); |
| + obj.put("rel-bounds", jsonRelBounds); |
| + } |
| + final int id = getId(); |
| + String resourceId = null; |
| + String pkgname = null; |
| + if (id != NO_ID) { |
| + final Resources r = mResources; |
| + if (Resources.resourceHasPackage(id) && r != null) { |
| + try { |
| + switch (id & 0xff000000) { |
| + case 0x01000000: |
| + pkgname = "android"; |
| + break; |
| + default: |
| + pkgname = r.getResourcePackageName(id); |
| + break; |
| + } |
| + String typename = r.getResourceTypeName(id); |
| + String entryname = r.getResourceEntryName(id); |
| + resourceId = pkgname + ":" + typename + "/" + entryname; |
| + } catch (Resources.NotFoundException e) { |
| + Log.e("RICO", "Resources not found for some view instance."); |
| + } |
| + } |
| + } |
| + obj.put("resource-id", resourceId); |
| + obj.put("package", pkgname); |
| + obj.put("content-desc", mContentDescription); |
| + return obj; |
| + } |
| + |
| + |
| public String toString() { |
| StringBuilder out = new StringBuilder(128); |
| out.append(getClass().getName()); |
| @@ -4616,12 +4733,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| } |
| String typename = r.getResourceTypeName(id); |
| String entryname = r.getResourceEntryName(id); |
| - out.append(" "); |
| - out.append(pkgname); |
| - out.append(":"); |
| - out.append(typename); |
| - out.append("/"); |
| - out.append(entryname); |
| + out.append(String.format(" %s:%s/%s", pkgname, typename, entryname)); |
| } catch (Resources.NotFoundException e) { |
| } |
| } |
| @@ -5200,10 +5312,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| final boolean result; |
| final ListenerInfo li = mListenerInfo; |
| if (li != null && li.mOnClickListener != null) { |
| + logEvent("click", true); |
| playSoundEffect(SoundEffectConstants.CLICK); |
| li.mOnClickListener.onClick(this); |
| result = true; |
| } else { |
| + logEvent("click", false); |
| result = false; |
| } |
| |
| @@ -5240,7 +5354,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| boolean handled = false; |
| ListenerInfo li = mListenerInfo; |
| if (li != null && li.mOnLongClickListener != null) { |
| + logEvent("long_click", true); |
| handled = li.mOnLongClickListener.onLongClick(View.this); |
| + } else { |
| + logEvent("long_click", false); |
| } |
| if (!handled) { |
| handled = showContextMenu(); |
| @@ -5290,6 +5407,80 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| } |
| |
| /** |
| + * RICO: Returns the current Activity. |
| + */ |
| + private Activity getActivity() { |
| + Context context = getContext(); |
| + while (context instanceof ContextWrapper) { |
| + if (context instanceof Activity) { |
| + return (Activity) context; |
| + } |
| + context = ((ContextWrapper) context).getBaseContext(); |
| + } |
| + return null; |
| + } |
| + |
| + private String getDisplayId() { |
| + StringBuilder out = new StringBuilder(128); |
| + final int id = getId(); |
| + if (id != NO_ID) { |
| + final Resources resources = mResources; |
| + if (Resources.resourceHasPackage(id) && resources != null) { |
| + try { |
| + String pkgname; |
| + switch (id & 0xff000000) { |
| + case 0x01000000: |
| + pkgname = "android"; |
| + break; |
| + default: |
| + pkgname = resources.getResourcePackageName(id); |
| + break; |
| + } |
| + String typename = resources.getResourceTypeName(id); |
| + String entryname = resources.getResourceEntryName(id); |
| + out.append(pkgname); |
| + out.append(":"); |
| + out.append(typename); |
| + out.append("/"); |
| + out.append(entryname); |
| + } catch (Resources.NotFoundException e) { |
| + return "id_not_found"; |
| + } |
| + } |
| + } |
| + return out.toString(); |
| + } |
| + |
| + /** |
| + * RICO: Outputs information to logcat when events are handled by this view. |
| + * @hide |
| + */ |
| + protected void logEvent(String type, boolean through) { |
| + String pointer = Integer.toHexString(System.identityHashCode(this)); |
| + String id = this.getDisplayId(); |
| + String classname = this.getClass().getName(); |
| + String content = ((mContentDescription == null) ? "" : (String) mContentDescription); |
| + String position = ""; |
| + if (mAttachInfo == null) { |
| + position = String.format("Rel[%s,%s,%s,%s]", mLeft, mTop, mRight, mBottom); |
| + } else { |
| + Rect bounds = mAttachInfo.mTmpInvalRect; |
| + getBoundsOnScreen(bounds, true); |
| + position = String.format("Abs[%s,%s,%s,%s]", bounds.left, bounds.top, |
| + bounds.right, bounds.bottom); |
| + } |
| + String listener = Boolean.toString(through); |
| + String activityName = "not_available"; |
| + if (getActivity() != null && getActivity().getComponentName() != null) { |
| + activityName = getActivity().getComponentName().flattenToString(); |
| + } |
| + String stringFormat = "RicoBegin:%s:%s:%s:%s:%s:%s:%s:%s:RicoEnd"; |
| + String outString = String.format(stringFormat, type, pointer, id, classname, content, |
| + position, listener, activityName); |
| + Log.d("RICO", outString); |
| + } |
| + |
| + /** |
| * Bring up the context menu for this view. |
| * |
| * @return Whether a context menu was displayed. |
| @@ -10731,7 +10922,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| */ |
| protected void onScrollChanged(int l, int t, int oldl, int oldt) { |
| notifySubtreeAccessibilityStateChangedIfNeeded(); |
| - |
| + logEvent("scroll", true); |
| if (AccessibilityManager.getInstance(mContext).isEnabled()) { |
| postSendViewScrolledAccessibilityEventCallback(); |
| } |
| @@ -19697,8 +19888,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, |
| //noinspection SimplifiableIfStatement |
| if (li != null && li.mOnDragListener != null && (mViewFlags & ENABLED_MASK) == ENABLED |
| && li.mOnDragListener.onDrag(this, event)) { |
| + logEvent("drag", true); |
| return true; |
| } |
| + logEvent("drag", false); |
| return onDragEvent(event); |
| } |
| |
| diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java |
| index 9569422..7290c06 100644 |
| --- a/core/java/android/view/ViewRootImpl.java |
| +++ b/core/java/android/view/ViewRootImpl.java |
| @@ -82,6 +82,11 @@ import com.android.internal.policy.PhoneFallbackEventHandler; |
| import com.android.internal.view.BaseSurfaceHolder; |
| import com.android.internal.view.RootViewSurfaceTaker; |
| |
| +import org.json.JSONArray; |
| +import org.json.JSONException; |
| +import org.json.JSONObject; |
| +import org.json.JSONStringer; |
| + |
| import java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| @@ -5507,6 +5512,14 @@ public final class ViewRootImpl implements ViewParent, |
| mView.debug(); |
| } |
| |
| + /** |
| + * RICO: Dumps the view hierarchy information in the supplied JSONObject starting from the root |
| + * view. |
| + */ |
| + public void dumpRoot(JSONObject view) throws JSONException { |
| + view.put("root", dumpViewHierarchyAsJson(mView)); |
| + } |
| + |
| public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { |
| String innerPrefix = prefix + " "; |
| writer.print(prefix); writer.println("ViewRoot:"); |
| @@ -5537,6 +5550,29 @@ public final class ViewRootImpl implements ViewParent, |
| dumpViewHierarchy(innerPrefix, writer, mView); |
| } |
| |
| + /* |
| + * RICO: Recursively traverses the view hierarchy and returns a JSONObject |
| + * that contains this hierarchy and also properties of each of the views. |
| + */ |
| + private JSONObject dumpViewHierarchyAsJson(View view) throws JSONException { |
| + if (view == null) { |
| + return null; |
| + } |
| + JSONObject viewJsonObject = view.toJson(); |
| + if (view instanceof ViewGroup) { |
| + ViewGroup grp = (ViewGroup)view; |
| + final int numChildren = grp.getChildCount(); |
| + if (numChildren > 0) { |
| + JSONArray children = new JSONArray(); |
| + for (int i = 0; i < numChildren; i++) { |
| + children.put(dumpViewHierarchyAsJson(grp.getChildAt(i))); |
| + } |
| + viewJsonObject.put("children", children); |
| + } |
| + } |
| + return viewJsonObject; |
| + } |
| + |
| private void dumpViewHierarchy(String prefix, PrintWriter writer, View view) { |
| writer.print(prefix); |
| if (view == null) { |
| diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java |
| index 606168c..a428f3d 100644 |
| --- a/core/java/android/view/WindowManagerGlobal.java |
| +++ b/core/java/android/view/WindowManagerGlobal.java |
| @@ -173,6 +173,51 @@ public final class WindowManagerGlobal { |
| } |
| } |
| |
| + /** RICO |
| + * @return True if the keyboard is deployed, false otherwise. |
| + */ |
| + public boolean isKeyboardDeployed() { |
| + boolean retVal = false; |
| + try { |
| + retVal = sWindowManagerService.isKeyboardDeployed(); |
| + } catch (RemoteException re) { |
| + re.printStackTrace(); |
| + } |
| + return retVal; |
| + } |
| + |
| + /** |
| + * RICO: Obtains the root UI Element (of ViewRootImpl class) of the current |
| + * top window. Calls the custom getCurrentFlag() method in the |
| + * WindowManagerService class to first obtain the flag of the current top window, |
| + * then compares flag with each view roots of all windows. |
| + */ |
| + public ViewRootImpl getCurrentFocusRoot() { |
| + ViewRootImpl curViewRoot = null; |
| + synchronized (mLock) { |
| + int curFlag = -1; |
| + try { |
| + curFlag = sWindowManagerService.getCurrentFlag(); |
| + } catch (RemoteException re) { |
| + re.printStackTrace(); |
| + } |
| + if (curFlag == -1) { |
| + return null; |
| + } |
| + |
| + final int numRoots = mRoots.size(); |
| + String[] mViewRoots = new String[numRoots + 1]; |
| + for (int i = 0; i < numRoots; ++i) { |
| + ViewRootImpl ithRoot = mRoots.get(i); |
| + if (ithRoot.mWindowAttributes.flags == curFlag) { |
| + curViewRoot = ithRoot; |
| + } |
| + } |
| + |
| + } |
| + return curViewRoot; |
| + } |
| + |
| public String[] getViewRootNames() { |
| synchronized (mLock) { |
| final int numRoots = mRoots.size(); |
| diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java |
| index 0c4b60b..dfb95c4 100644 |
| --- a/core/java/android/widget/TextView.java |
| +++ b/core/java/android/widget/TextView.java |
| @@ -144,6 +144,11 @@ import android.widget.RemoteViews.RemoteView; |
| import com.android.internal.util.FastMath; |
| import com.android.internal.widget.EditableInputConnection; |
| |
| +import org.json.JSONArray; |
| +import org.json.JSONException; |
| +import org.json.JSONObject; |
| +import org.json.JSONStringer; |
| + |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| @@ -3979,6 +3984,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener |
| } |
| } |
| |
| + /** |
| + * RICO: Adds a text field to the JSON object. |
| + * @hide |
| + */ |
| + @Override |
| + public JSONObject toJson() throws JSONException { |
| + Typeface tf = getTypeface(); |
| + JSONObject obj = super.toJson(); |
| + obj.put("text", mText); |
| + if (tf != null) { |
| + obj.put("font-family", tf.getFontFamilyName()); |
| + } |
| + return obj; |
| + } |
| + |
| + |
| @Override |
| public void drawableHotspotChanged(float x, float y) { |
| super.drawableHotspotChanged(x, y); |
| diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java |
| index db42314..b8911af 100644 |
| --- a/graphics/java/android/graphics/Typeface.java |
| +++ b/graphics/java/android/graphics/Typeface.java |
| @@ -73,6 +73,11 @@ public class Typeface { |
| */ |
| public long native_instance; |
| |
| + /** |
| + * RICO: Stores font family if it was specified when creating the Typeface. |
| + */ |
| + private String mFontFamilyName = "default"; |
| + |
| // Style |
| public static final int NORMAL = 0; |
| public static final int BOLD = 1; |
| @@ -114,7 +119,10 @@ public class Typeface { |
| */ |
| public static Typeface create(String familyName, int style) { |
| if (sSystemFontMap != null) { |
| - return create(sSystemFontMap.get(familyName), style); |
| + // RICO |
| + Typeface f = create(sSystemFontMap.get(familyName), style); |
| + f.mFontFamilyName = familyName; |
| + return f; |
| } |
| return null; |
| } |
| @@ -161,6 +169,10 @@ public class Typeface { |
| } |
| styles.put(style, typeface); |
| |
| + if (family != null) { |
| + typeface.mFontFamilyName = family.mFontFamilyName; |
| + } |
| + |
| return typeface; |
| } |
| |
| @@ -373,6 +385,14 @@ public class Typeface { |
| return mStyle == typeface.mStyle && native_instance == typeface.native_instance; |
| } |
| |
| + /** |
| + * RICO: Returns font family name. |
| + * @hide |
| + */ |
| + public String getFontFamilyName() { |
| + return mFontFamilyName; |
| + } |
| + |
| @Override |
| public int hashCode() { |
| /* |
| diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java |
| index ba916ad..44d7e9e 100644 |
| --- a/services/core/java/com/android/server/am/ActivityManagerService.java |
| +++ b/services/core/java/com/android/server/am/ActivityManagerService.java |
| @@ -232,6 +232,7 @@ import android.view.WindowManager; |
| import dalvik.system.VMRuntime; |
| |
| import java.io.BufferedInputStream; |
| +import java.io.BufferedReader; |
| import java.io.BufferedOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| @@ -241,10 +242,14 @@ import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| +import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.lang.ref.WeakReference; |
| +import java.net.InetSocketAddress; |
| +import java.net.ServerSocket; |
| +import java.net.Socket; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| @@ -418,6 +423,9 @@ public final class ActivityManagerService extends ActivityManagerNative |
| // Lower delay than APP_BOOST_MESSAGE_DELAY to disable the boost |
| static final int APP_BOOST_TIMEOUT = 2500; |
| |
| + // RICO: View server runs on this port and handles requests for dumps of the view hierarchy. |
| + static final int VIEW_SERVER_PORT = 1699; |
| + |
| private static native int nativeMigrateToBoost(); |
| private static native int nativeMigrateFromBoost(); |
| private boolean mIsBoosted = false; |
| @@ -456,6 +464,22 @@ public final class ActivityManagerService extends ActivityManagerNative |
| return (isFg) ? mFgBroadcastQueue : mBgBroadcastQueue; |
| } |
| |
| + /* |
| + * RICO: Thread that runs the view server. |
| + */ |
| + static Thread sDumperThread = null; |
| + |
| + /* |
| + * RICO: Stores the current top activity obtained from the modified window manager. |
| + */ |
| + volatile ActivityRecord mCurrentTopActivity = null; |
| + |
| + /* |
| + * RICO: Initialized by the dumper thread and used to dump the JSON representation of the |
| + * view hierarchy. |
| + */ |
| + ParcelFileDescriptor pfd = null; |
| + |
| /** |
| * Activity we have told the window manager to have key focus. |
| */ |
| @@ -13109,6 +13133,9 @@ public final class ActivityManagerService extends ActivityManagerNative |
| } |
| } |
| return; |
| + } else if ("start-view-server".equals(cmd)) { |
| + Log.d("RICO", "RicoBegin:Starting Rico View Server:RicoEnd"); |
| + dumpView(fd, pw, args, opti, dumpAll); |
| } else { |
| // Dumping a single activity? |
| if (!dumpActivity(fd, pw, cmd, args, opti, dumpAll)) { |
| @@ -13991,6 +14018,110 @@ public final class ActivityManagerService extends ActivityManagerNative |
| return true; |
| } |
| |
| + |
| + /** |
| + * RICO: This command dumps the view hierarchy of the top window of the top |
| + * activity when a message is received from the client. It uses a custom |
| + * dumpActivity() method in ActivityThread class to obtain the top window |
| + * from an activity. |
| + */ |
| + protected void dumpView(FileDescriptor fd, PrintWriter pw, String[] args, int opti, |
| + boolean dumpAll) { |
| + |
| + // If sDumper Thread is running, we ignore the command to start it. |
| + if (sDumperThread != null && sDumperThread.isAlive()) { |
| + Log.d("RICO", "RicoBegin:Rico View Server is already running!:RicoEnd"); |
| + return; |
| + } |
| + |
| + final FileDescriptor threadFd = fd; |
| + final String[] newArgs = new String[args.length - opti]; |
| + System.arraycopy(args, opti, newArgs, 0, args.length - opti); |
| + final PrintWriter threadPw = pw; |
| + |
| + sDumperThread = new Thread(new Runnable() { |
| + public void run() { |
| + String request; |
| + String response; |
| + Boolean stopServer = false; |
| + try { |
| + ServerSocket server = new ServerSocket(VIEW_SERVER_PORT); |
| + while (true) { |
| + Socket socket = server.accept(); |
| + BufferedReader in = |
| + new BufferedReader(new InputStreamReader(socket.getInputStream())); |
| + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); |
| + |
| + pfd = ParcelFileDescriptor.fromSocket(socket); |
| + |
| + while (true) { |
| + request = in.readLine(); |
| + // If client closes abruptly, a null value is returned. |
| + if (request == null) { |
| + break; |
| + } |
| + |
| + String[] requestParts = request.split(" "); |
| + String command = null; |
| + String requestId = null; |
| + if (requestParts.length > 0) { |
| + command = requestParts[0]; |
| + } |
| + if (requestParts.length > 1) { |
| + requestId = requestParts[1]; |
| + } |
| + |
| + // If an "s" is received, stop the server. |
| + // If a "d" is received, dump the view. |
| + if ("s".equals(command)) { |
| + stopServer = true; |
| + break; |
| + } else if ("d".equals(command)) { |
| + if (requestId == null) { |
| + out.print("{ \"request_id\": null"); |
| + Log.d("RICO", "RicoBegin:Request_ID: None:RicoEnd"); |
| + } else { |
| + out.print(String.format("{ \"request_id\": \"%s\"", requestId)); |
| + Log.d("RICO", String.format("RicoBegin:Request_ID: %s:RicoEnd", |
| + requestId)); |
| + } |
| + |
| + mCurrentTopActivity = mStackSupervisor.getTopStackTopActivity(); |
| + |
| + /** |
| + * If an active activity is detected, the UI information from that |
| + * activity will be dumped as JSON. |
| + */ |
| + if (mCurrentTopActivity == null) { |
| + out.println(",\n\"activity_name\": null,"); |
| + } else { |
| + out.println(String.format(",\n\"activity_name\": \"%s\",", |
| + mCurrentTopActivity.shortComponentName)); |
| + dumpActivityToStream(" ", threadFd, out, mCurrentTopActivity, |
| + newArgs, false); |
| + } |
| + out.println("}"); |
| + out.println("RICO_JSON_END"); |
| + out.flush(); |
| + } |
| + } // End of inner while loop (reads each line of input from client). |
| + Log.d("RICO", "RicoBegin:Closing Socket:RicoEnd"); |
| + socket.close(); |
| + if (stopServer) { |
| + break; |
| + } |
| + } // End of outer while loop (runs once for each client). |
| + Log.d("RICO", "RicoBegin:Stopping Server:RicoEnd"); |
| + server.close(); |
| + } catch (IOException e) { |
| + e.printStackTrace(); |
| + } |
| + } |
| + }); |
| + sDumperThread.start(); |
| + } |
| + |
| + |
| /** |
| * Invokes IApplicationThread.dumpActivity() on the thread of the specified activity if |
| * there is a thread associated with the activity. |
| @@ -14029,6 +14160,38 @@ public final class ActivityManagerService extends ActivityManagerNative |
| } |
| } |
| |
| + /** |
| + * RICO: Calls the dumpActivity() method on the ActivityThread with an |
| + * additional parameter called stream. |
| + */ |
| + private void dumpActivityToStream(String prefix, FileDescriptor fd, PrintWriter pw, |
| + final ActivityRecord r, String[] args, boolean dumpAll) { |
| + String innerPrefix = prefix + " "; |
| + |
| + if (r.app != null && r.app.thread != null && pfd != null) { |
| + // Flush anything that is already in the PrintWriter since the thread is going |
| + // to write to the file descriptor directly. |
| + pw.flush(); |
| + try { |
| + TransferPipe tp = new TransferPipe(); |
| + FileDescriptor nfd = pfd.getFileDescriptor(); |
| + String[] newArgs = new String[args.length + 1]; |
| + newArgs[args.length] = "stream"; |
| + try { |
| + r.app.thread.dumpActivity(tp.getWriteFd().getFileDescriptor(), |
| + r.appToken, innerPrefix, newArgs); |
| + tp.go(nfd); |
| + } finally { |
| + tp.kill(); |
| + } |
| + } catch (IOException e) { |
| + pw.println(innerPrefix + "Failure while dumping the activity: " + e); |
| + } catch (RemoteException e) { |
| + pw.println(innerPrefix + "Got a RemoteException while dumping the activity"); |
| + } |
| + } |
| + } |
| + |
| void dumpBroadcastsLocked(FileDescriptor fd, PrintWriter pw, String[] args, |
| int opti, boolean dumpAll, String dumpPackage) { |
| boolean needSep = false; |
| diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java |
| index 6e34876..b049366 100644 |
| --- a/services/core/java/com/android/server/am/ActivityStack.java |
| +++ b/services/core/java/com/android/server/am/ActivityStack.java |
| @@ -4206,6 +4206,22 @@ final class ActivityStack { |
| } |
| } |
| |
| + /** |
| + * RICO: Gets the ActivityRecord of the activity that is at the top of the stack. |
| + * The activity that is appended last to the list called mActivities is |
| + * typically the current activity that is being used by the user. |
| + */ |
| + ActivityRecord getCurrentTopActivity() { |
| + if (mTaskHistory.size() == 0) { |
| + return null; |
| + } |
| + TaskRecord topTask = mTaskHistory.get(mTaskHistory.size() - 1); |
| + if (topTask.mActivities.size() == 0) { |
| + return null; |
| + } |
| + return topTask.mActivities.get(topTask.mActivities.size() - 1); |
| + } |
| + |
| boolean dumpActivitiesLocked(FileDescriptor fd, PrintWriter pw, boolean dumpAll, |
| boolean dumpClient, String dumpPackage, boolean needSep, String header) { |
| boolean printed = false; |
| diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java |
| index 17a86ca..f0883b9 100644 |
| --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java |
| +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java |
| @@ -3554,6 +3554,24 @@ public final class ActivityStackSupervisor implements DisplayListener { |
| return false; |
| } |
| |
| + |
| + /* |
| + * RICO: Gets the stacks of activities and calls the custom method getCurrentTopActivity() |
| + * method to obtain the top activity of that stack. This is typically the activity that |
| + * the user is interacting with. |
| + */ |
| + ActivityRecord getTopStackTopActivity() { |
| + if (mActivityDisplays.size() == 0) { |
| + return null; |
| + } |
| + ActivityDisplay activityDisplay = mActivityDisplays.valueAt(0); |
| + ArrayList<ActivityStack> stacks = activityDisplay.mStacks; |
| + if (stacks.size() == 0) { |
| + return null; |
| + } |
| + return stacks.get(stacks.size() - 1).getCurrentTopActivity(); |
| + } |
| + |
| boolean dumpActivitiesLocked(FileDescriptor fd, PrintWriter pw, boolean dumpAll, |
| boolean dumpClient, String dumpPackage) { |
| boolean printed = false; |
| diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java |
| index c40947b..0d9b87a 100644 |
| --- a/services/core/java/com/android/server/wm/WindowManagerService.java |
| +++ b/services/core/java/com/android/server/wm/WindowManagerService.java |
| @@ -3956,6 +3956,27 @@ public class WindowManagerService extends IWindowManager.Stub |
| return config; |
| } |
| |
| + /** |
| + * Gets the flag associated with a given window. |
| + * |
| + * @return The flag of the currently focused window or -1 if the window does not exist. |
| + */ |
| + public int getCurrentFlag() { |
| + if (mCurrentFocus == null) { |
| + return -1; |
| + } |
| + return mCurrentFocus.mAttrs.flags; |
| + } |
| + |
| + /** |
| + * Gets state of the keyboard. |
| + * |
| + * @return True if the keyboard is deployed, false otherwise. |
| + */ |
| + public boolean isKeyboardDeployed() { |
| + return (mInputMethodWindow != null); |
| + } |
| + |
| private Configuration updateOrientationFromAppTokensLocked( |
| Configuration currentConfig, IBinder freezeThisOneIfNeeded) { |
| if (!mDisplayReady) { |