Collecting some data on notification panel gestures.
Look for it in /sdcard/statusbar_gestures.dat, in "JSON
lines" format: one list of gestures per line; each gesture
is itself a list of objects representing motion events and
tags (annotations).
Exploded example:
[ // list of gestures
[ // this starts a gesture
{"type":"motion",
"time":1347697, // in SystemClock.uptimeMillis() base,
// like MotionEvents
"action":"down", // down, up, move, cancel, else numeric
"x":277.61,
"y":1.00
},
{"type":"tag",
"time":1347701,
"tag":"tracking", // "tracking" or "fling"
"info":"collapsed" // extra stuff
},
... // more events
],
... // more gestures
]
// newline
[ // another list of gestures
...
]
...
Change-Id: Ifacbf03749c879cd82fb899289fb79a4bdd4fc3b
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/GestureRecorder.java b/packages/SystemUI/src/com/android/systemui/statusbar/GestureRecorder.java
new file mode 100755
index 0000000..c4ed0e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/GestureRecorder.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.android.systemui.statusbar;
+
+import java.io.BufferedWriter;
+import java.io.FileDescriptor;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.LinkedList;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.view.MotionEvent;
+
+/**
+ * Convenience class for capturing gestures for later analysis.
+ */
+public class GestureRecorder {
+ public static final boolean DEBUG = true; // for now
+ public static final String TAG = GestureRecorder.class.getSimpleName();
+
+ public class Gesture {
+ public abstract class Record {
+ long time;
+ public abstract String toJson();
+ }
+ public class MotionEventRecord extends Record {
+ public MotionEvent event;
+ public MotionEventRecord(long when, MotionEvent event) {
+ this.time = when;
+ this.event = event.copy();
+ }
+ String actionName(int action) {
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ return "down";
+ case MotionEvent.ACTION_UP:
+ return "up";
+ case MotionEvent.ACTION_MOVE:
+ return "move";
+ case MotionEvent.ACTION_CANCEL:
+ return "cancel";
+ default:
+ return String.valueOf(action);
+ }
+ }
+ public String toJson() {
+ return String.format("{\"type\":\"motion\", \"time\":%d, \"action\":\"%s\", \"x\":%.2f, \"y\":%.2f}",
+ this.time,
+ actionName(this.event.getAction()),
+ this.event.getRawX(),
+ this.event.getRawY()
+ );
+ }
+ }
+ public class TagRecord extends Record {
+ public String tag, info;
+ public TagRecord(long when, String tag, String info) {
+ this.time = when;
+ this.tag = tag;
+ this.info = info;
+ }
+ public String toJson() {
+ return String.format("{\"type\":\"tag\", \"time\":%d, \"tag\":\"%s\", \"info\":\"%s\"}",
+ this.time,
+ this.tag,
+ this.info
+ );
+ }
+ }
+ private LinkedList<Record> mRecords = new LinkedList<Record>();
+ private HashSet<String> mTags = new HashSet<String>();
+ long mDownTime = -1;
+ boolean mComplete = false;
+
+ public void add(MotionEvent ev) {
+ mRecords.add(new MotionEventRecord(ev.getEventTime(), ev));
+ if (mDownTime < 0) {
+ mDownTime = ev.getDownTime();
+ } else {
+ if (mDownTime != ev.getDownTime()) {
+ // TODO: remove
+ throw new RuntimeException("Assertion failure in GestureRecorder: event downTime ("
+ +ev.getDownTime()+") does not match gesture downTime ("+mDownTime+")");
+ }
+ }
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mComplete = true;
+ }
+ }
+ public void tag(long when, String tag, String info) {
+ mRecords.add(new TagRecord(when, tag, info));
+ mTags.add(tag);
+ }
+ public boolean isComplete() {
+ return mComplete;
+ }
+ public String toJson() {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ sb.append("[");
+ for (Record r : mRecords) {
+ if (!first) sb.append(", ");
+ first = false;
+ sb.append(r.toJson());
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+ }
+
+ // -=-=-=-=-=-=-=-=-=-=-=-
+
+ static final long SAVE_DELAY = 5000; // ms
+ static final int SAVE_MESSAGE = 6351;
+
+ private LinkedList<Gesture> mGestures;
+ private Gesture mCurrentGesture;
+ private int mLastSaveLen = -1;
+ private String mLogfile;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == SAVE_MESSAGE) {
+ save();
+ }
+ }
+ };
+
+ public GestureRecorder(String filename) {
+ mLogfile = filename;
+ mGestures = new LinkedList<Gesture>();
+ mCurrentGesture = null;
+ }
+
+ public void add(MotionEvent ev) {
+ synchronized (mGestures) {
+ if (mCurrentGesture == null || mCurrentGesture.isComplete()) {
+ mCurrentGesture = new Gesture();
+ mGestures.add(mCurrentGesture);
+ }
+ mCurrentGesture.add(ev);
+ }
+ saveLater();
+ }
+
+ public void tag(long when, String tag, String info) {
+ synchronized (mGestures) {
+ if (mCurrentGesture == null) {
+ mCurrentGesture = new Gesture();
+ mGestures.add(mCurrentGesture);
+ }
+ mCurrentGesture.tag(when, tag, info);
+ }
+ saveLater();
+ }
+
+ public void tag(long when, String tag) {
+ tag(when, tag, null);
+ }
+
+ public void tag(String tag) {
+ tag(SystemClock.uptimeMillis(), tag, null);
+ }
+
+ public void tag(String tag, String info) {
+ tag(SystemClock.uptimeMillis(), tag, info);
+ }
+
+ /**
+ * Generates a JSON string capturing all completed gestures.
+ * Not threadsafe; call with a lock.
+ */
+ public String toJsonLocked() {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ sb.append("[");
+ int count = 0;
+ for (Gesture g : mGestures) {
+ if (!g.isComplete()) continue;
+ if (!first) sb.append("," );
+ first = false;
+ sb.append(g.toJson());
+ count++;
+ }
+ mLastSaveLen = count;
+ sb.append("]");
+ return sb.toString();
+ }
+
+ public String toJson() {
+ String s;
+ synchronized (mGestures) {
+ s = toJsonLocked();
+ }
+ return s;
+ }
+
+ public void saveLater() {
+ mHandler.removeMessages(SAVE_MESSAGE);
+ mHandler.sendEmptyMessageDelayed(SAVE_MESSAGE, SAVE_DELAY);
+ }
+
+ public void save() {
+ synchronized (mGestures) {
+ try {
+ BufferedWriter w = new BufferedWriter(new FileWriter(mLogfile, /*append=*/ true));
+ w.append(toJsonLocked() + "\n");
+ w.close();
+ mGestures.clear();
+ // If we have a pending gesture, push it back
+ if (!mCurrentGesture.isComplete()) {
+ mGestures.add(mCurrentGesture);
+ }
+ if (DEBUG) {
+ Slog.v(TAG, String.format("Wrote %d complete gestures to %s", mLastSaveLen, mLogfile));
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, String.format("Couldn't write gestures to %s", mLogfile), e);
+ mLastSaveLen = -1;
+ }
+ }
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ save();
+ if (mLastSaveLen >= 0) {
+ pw.println(String.valueOf(mLastSaveLen) + " gestures written to " + mLogfile);
+ } else {
+ pw.println("error writing gestures");
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 460619c..25de109 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -79,6 +79,7 @@
import com.android.systemui.recent.RecentTasksLoader;
import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.GestureRecorder;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationData.Entry;
import com.android.systemui.statusbar.RotationToggle;
@@ -242,6 +243,9 @@
DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+ // XXX: gesture research
+ private GestureRecorder mGestureRec = new GestureRecorder("/sdcard/statusbar_gestures.dat");
+
private int mNavigationIconHints = 0;
private final Animator.AnimatorListener mMakeIconsInvisible = new AnimatorListenerAdapter() {
@Override
@@ -1570,6 +1574,8 @@
}
}
+ mGestureRec.add(event);
+
if ((mDisabled & StatusBarManager.DISABLE_EXPAND) != 0) {
return false;
}
@@ -1604,6 +1610,7 @@
if (x >= edgeBorder && x < mDisplayMetrics.widthPixels - edgeBorder) {
prepareTracking(y, !mExpanded);// opening if we're not already fully visible
trackMovement(event);
+ mGestureRec.tag("tracking", mExpanded ? "expanded" : "collapsed");
}
}
} else if (mTracking) {
@@ -1654,6 +1661,8 @@
mFlingY = y;
}
mFlingVelocity = vel;
+ mGestureRec.tag("fling " + ((mFlingVelocity > 0) ? "open" : "closed"),
+ "v=" + mFlingVelocity);
mHandler.post(mPerformFling);
}
@@ -1938,6 +1947,9 @@
}
}
+ pw.print(" status bar gestures: ");
+ mGestureRec.dump(fd, pw, args);
+
mNetworkController.dump(fd, pw, args);
}