release-request-323db86e-b638-4d24-8eb1-d2e3bf4a9d1a-for-git_oc-mr1-release-4017779 snap-temp-L47900000064949209

Change-Id: Idf003230cc9df1881a0e63eb2d99a9e2d7bf8f0c
diff --git a/README.google b/README.google
new file mode 100644
index 0000000..fc3d2a0
--- /dev/null
+++ b/README.google
@@ -0,0 +1,91 @@
+Overview
+========
+
+This project is included in the internal Android source tree to support
+automated latency testing with Salad Fingers, a robot designed and built by
+Steve Pfetsch (spfetsch@). The three main components we use and modify are:
+
+  - the android app (android/)
+  - the WALT device firmware (arduino/)
+  - the TCP-to-serial bridge (pywalt/)
+
+Salad Fingers uses a single Teensyduino running WALT firmware to test multiple
+devices without human intervention. The devices under test are connected to the
+same host as the Teensy. An end-to-end connection for a single device (only one
+device can use the WALT at a time) looks like this:
+
+    Device ------ Host ------ Teensy
+  (android/)    (pywalt/)   (arduino/)
+
+For the device to communicate with the host over TCP on a physical USB
+connection, a "reverse" port has to be set up with adb. For example:
+
+  $ adb reverse tcp:50007 tcp:45454
+
+Any traffic the device sends to 127.0.0.1:50007 will come into the host on port
+45454 and vice versa. Port 50007 is defined in the app source, but the device
+port can be selected arbitrarily.
+
+The TCP-to-serial bridge runs on the host and connects the app's TCP pipe to
+the Teensy's serial pipe. However, there are two special commands the app can
+send to the bridge to synchronize the clocks between the device and the Teensy:
+"bridge sync" and "bridge update".
+
+This setup requires some modifications from the original source, which are
+explained in the next section, but behaves very similarly to a direct, Teensy-
+to-device USB connection.
+
+
+Modifications
+=============
+
+- Clock synchronization
+  Despite the reliability and accuracy of NTP, device and host wall clocks can
+  become significantly out of sync, especially if a device loses Wi-Fi
+  connectivity. To avoid this problem and take advantage of the low-latency
+  connection between the host and device, the clocks are synchronized based on
+  the time difference between when the bridge zeroed the Teensy's clock and
+  when the reply to the "bridge sync" command was sent to the device. This
+  required parallel changes in pywalt/ and android/.
+
+- Automation intents
+  The test scripts which control the robot (Salad Fingers) on which the Teensy
+  is mounted require the ability to control certain aspects of the app running
+  on a device. These are defined in a separate RobotAutomationEvent interface.
+  This required changes to android/.
+
+- Reverse port support
+  The WaltTcpConnection class was originally intended to communicate over a
+  true network from a Chromebook to a dedicated bridge with a specific IP
+  address. This address was changed to localhost in android/.
+
+- Strict networking and request ordering
+  To prevent crashes due to network accesses performed on the main thread,
+  which is disallowed in strict mode, all such accesses were moved to a
+  dedicated network thread. As a side benefit, all requests sent to the bridge
+  now wait for a response before the next request is sent. This complies with
+  the serial nature of the device and guarantees correct ordering. This
+  required changes to android/.
+
+- Hardware-specific firmware
+  The Teensy on Salad Fingers uses sensors that are not bundled with the
+  standard WALT hardware, so new thresholds for accelerometer shocks and photo-
+  diode readings were required to achieve accurate results. This required
+  changes to arduino/.
+
+
+Usage
+=====
+
+This project is not intended to be automatically built. Instead, as needed, the
+app will be built manually and checked in as a prebuilt that PTS will pick up.
+The firmware cannot be built or loaded automatically, as it requires a modded
+version of the Arduino project for Teensy. The required software can be loaded
+on a laptop to flash the Teensy mounted on Salad Fingers.
+
+
+See Also
+========
+
+Project metadata: vendor/google_meta/platform/external/walt/
+PTS integration:  vendor/google_testing/pts/tests/salad_fingers/
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java
index 109fcf8..af03e36 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/DragLatencyFragment.java
@@ -39,7 +39,8 @@
 import java.util.ArrayList;
 import java.util.Locale;
 
-public class DragLatencyFragment extends Fragment implements View.OnClickListener {
+public class DragLatencyFragment extends Fragment
+        implements View.OnClickListener, RobotAutomationListener {
 
     private SimpleLogger logger;
     private WaltDevice waltDevice;
@@ -332,6 +333,16 @@
         }
     }
 
+    public void onRobotAutomationEvent(String event) {
+        if (event.equals(RobotAutomationListener.RESTART_EVENT)) {
+            onClick(restartButton);
+        } else if (event.equals(RobotAutomationListener.START_EVENT)) {
+            onClick(startButton);
+        } else if (event.equals(RobotAutomationListener.FINISH_EVENT)) {
+            onClick(finishButton);
+        }
+    }
+
     private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() {
         @Override
         public void onReceive(WaltDevice.TriggerMessage tmsg) {
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java
index 7efee00..ac1df47 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/MainActivity.java
@@ -65,6 +65,10 @@
     private static final String TAG = "WALT";
     private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG = 2;
     private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SYSTRACE = 3;
+    private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG = 4;
+    private static final int PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG = 5;
+
+    private static final String LOG_FILENAME = "qstep_log.txt";
 
     private Toolbar toolbar;
     LocalBroadcastManager broadcastManager;
@@ -74,6 +78,8 @@
 
     public Handler handler = new Handler();
 
+    private Fragment mRobotAutomationFragment;
+
 
     /**
      * A method to display exceptions on screen. This is very useful because our USB port is taken
@@ -132,6 +138,41 @@
             autoRunFragment.setArguments(intent.getExtras());
             switchScreen(autoRunFragment, "Automated Test");
         }
+
+        // Handle robot automation originating from adb shell am
+        if (intent != null && Intent.ACTION_SEND.equals(intent.getAction())) {
+            Log.e(TAG, "Received Intent: " + intent.toString());
+            String test = intent.getStringExtra("StartTest");
+            if (test != null) {
+                Log.e(TAG, "Extras \"StartTest\" = " + test);
+                if ("TapLatencyTest".equals(test)) {
+                    mRobotAutomationFragment = new TapLatencyFragment();
+                    switchScreen(mRobotAutomationFragment, "Tap Latency");
+                } else if ("ScreenResponseTest".equals(test)) {
+                    mRobotAutomationFragment = new ScreenResponseFragment();
+                    switchScreen(mRobotAutomationFragment, "Screen Response");
+                } else if ("DragLatencyTest".equals(test)) {
+                    mRobotAutomationFragment = new DragLatencyFragment();
+                    switchScreen(mRobotAutomationFragment, "Drag Latency");
+                }
+            }
+
+            String robotEvent = intent.getStringExtra("RobotAutomationEvent");
+            if (robotEvent != null && mRobotAutomationFragment != null) {
+                Log.e(TAG, "Received robot automation event=\"" + robotEvent + "\", Fragment = " +
+                        mRobotAutomationFragment);
+                // Writing and clearing the log is not fragment-specific, so handle them here.
+                if (robotEvent.equals(RobotAutomationListener.WRITE_LOG_EVENT)) {
+                    attemptSaveLog();
+                } else if (robotEvent.equals(RobotAutomationListener.CLEAR_LOG_EVENT)) {
+                    attemptClearLog();
+                } else {
+                    // All other robot automation events are forwarded to the current fragment.
+                    ((RobotAutomationListener) mRobotAutomationFragment)
+                            .onRobotAutomationEvent(robotEvent);
+                }
+            }
+        }
     }
 
     @Override
@@ -389,6 +430,30 @@
         }
     }
 
+    private void attemptSaveLog() {
+        int currentPermission = ContextCompat.checkSelfPermission(this,
+                Manifest.permission.WRITE_EXTERNAL_STORAGE);
+        if (currentPermission == PackageManager.PERMISSION_GRANTED) {
+            saveLogToFile();
+        } else {
+            ActivityCompat.requestPermissions(this,
+                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+                    PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG);
+        }
+    }
+
+    private void attemptClearLog() {
+        int currentPermission = ContextCompat.checkSelfPermission(this,
+                Manifest.permission.WRITE_EXTERNAL_STORAGE);
+        if (currentPermission == PackageManager.PERMISSION_GRANTED) {
+            clearLogFile();
+        } else {
+            ActivityCompat.requestPermissions(this,
+                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+                    PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG);
+        }
+    }
+
     @Override
     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@@ -401,6 +466,12 @@
             case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_SHARE_LOG:
                 attemptSaveAndShareLog();
                 break;
+            case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_WRITE_LOG:
+                attemptSaveLog();
+                break;
+            case PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE_CLEAR_LOG:
+                attemptClearLog();
+                break;
         }
     }
 
@@ -414,22 +485,19 @@
         // is frowned upon, but deliberately giving permissions as part of the intent is
         // way too cumbersome.
 
-        String fname = "qstep_log.txt";
         // A reasonable world readable location,on many phones it's /storage/emulated/Documents
         // TODO: make this location configurable?
         File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
         File file = null;
         FileOutputStream outStream = null;
 
-        Date now = new Date();
-        logger.log("Saving log to:\n" + path.getPath() + "/" + fname);
-        logger.log("On: " + now.toString());
-
         try {
             if (!path.exists()) {
                 path.mkdirs();
             }
-            file = new File(path, fname);
+            file = new File(path, LOG_FILENAME);
+            logger.log("Saving log to: " + file + " at " + new Date());
+
             outStream = new FileOutputStream(file);
             outStream.write(logger.getLogText().getBytes());
 
@@ -437,11 +505,22 @@
             logger.log("Log saved");
         } catch (Exception e) {
             e.printStackTrace();
-            logger.log("Exception:\n" + e.getMessage());
+            logger.log("Failed to write log: " + e.getMessage());
         }
         return file.getPath();
     }
 
+    public void clearLogFile() {
+        File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
+        try {
+            File file = new File(path, LOG_FILENAME);
+            file.delete();
+        } catch (Exception e) {
+            e.printStackTrace();
+            logger.log("Failed to clear log: " + e.getMessage());
+        }
+    }
+
     public void shareLogFile(String filepath) {
         File file = new File(filepath);
         logger.log("Firing Intent.ACTION_SEND for file:");
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java
index 1a42eb5..4db9358 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/RemoteClockInfo.java
@@ -16,10 +16,6 @@
 
 package org.chromium.latency.walt;
 
-import android.util.Log;
-
-import java.lang.reflect.Method;
-
 /**
  * Representation of our best knowledge of the remote clock.
  * All time variables here are stored in microseconds.
@@ -27,18 +23,18 @@
  * Which time reporting function is used locally on Android:
  * This app uses SystemClock.uptimeMillis() for keeping local time which, up to
  * units, is the same time reported by System.nanoTime() and by
- * clock_gettime(CLOCK_MONOTONIC, &ts) from time.h and is, roughly, the time
+ * clock_gettime(CLOCK_MONOTONIC, &ts) from time.h and is, roughly, the time
  * elapsed since last boot, excluding sleep time.
  *
- * base_time is the local Android time when remote clock was zeroed.
+ * base_time is the local monotonic clock time when remote clock was zeroed.
  *
  * micros() is our best available approximation of the current reading of the remote clock.
  *
- * Immediately after synchronization minLag is set to zero and the remote clock guaranteed to lag
- * behind what micros() reports by at most maxLag.
+ * Immediately after synchronization, minLag is set to zero and the remote clock is guaranteed to
+ * lag behind what micros() reports by at most maxLag.
  *
  * Immediately after synchronization or an update of the bounds (minLag, maxLag) the following holds
- * t_remote + minLag < micros() < t_rmote + maxLag
+ * t_remote + minLag &lt; micros() &lt; t_remote + maxLag
  *
  * For more details about clock synchronization refer to
  * https://github.com/google/walt/blob/master/android/WALT/app/src/main/jni/README.md
@@ -50,67 +46,19 @@
     public int maxLag;
     public long baseTime;
 
-
-    public long micros() {
-        return microTime() - baseTime;
-    }
-
     public static long microTime() {
         return System.nanoTime() / 1000;
     }
 
-
-    /**
-     Find the wall time when uptime was zero = CLOCK_REALTIME - CLOCK_MONOTONIC
-
-     Needed for TCP bridge because Python prior to 3.3 has no direct access to CLOCK_MONOTONIC
-     so the bridge returns timestamps as wall time and we need to convert them to CLOCK_MONOTONIC.
-
-     See:
-     [1] https://docs.python.org/3/library/time.html#time.CLOCK_MONOTONIC
-     [2] http://stackoverflow.com/questions/14270300/what-is-the-difference-between-clock-monotonic-clock-monotonic-raw
-     [3] http://stackoverflow.com/questions/1205722/how-do-i-get-monotonic-time-durations-in-python
-
-     android.os.SystemClock.currentTimeMicros() is hidden by @hide which means it can't be called
-     directly - calling it via reflection.
-
-     See:
-     http://stackoverflow.com/questions/17035271/what-does-hide-mean-in-the-android-source-code
-     */
-    public static long uptimeZero() {
-        long t = -1;
-        long dt = Long.MAX_VALUE;
-        try {
-            Class cls = Class.forName("android.os.SystemClock");
-            Method myTimeGetter = cls.getMethod("currentTimeMicro");
-            t = (long) myTimeGetter.invoke(null);
-            dt = t - microTime();
-        } catch (Exception e) {
-            Log.i("WALT.uptimeZero", e.getMessage());
-        }
-
-        return dt;
-    }
-
-    public static long currentTimeMicro() {
-
-        long t = -1;
-        try {
-            Class cls = Class.forName("android.os.SystemClock");
-            Method myTimeGetter = cls.getMethod("currentTimeMicro");
-            t = (long) myTimeGetter.invoke(null);
-        } catch (Exception e) {
-            Log.i("WALT.currentTimeMicro", e.getMessage());
-        }
-
-        return t;
+    public long micros() {
+        return microTime() - baseTime;
     }
 
     public int getMeanLag() {
         return (minLag + maxLag) / 2;
     }
 
-    public String toString(){
+    public String toString() {
         return "Remote clock [us]: current time = " + micros() + " baseTime = " + baseTime +
                 " lagBounds = (" + minLag + ", " + maxLag + ")";
     }
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/RobotAutomationListener.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/RobotAutomationListener.java
new file mode 100644
index 0000000..47a21ba
--- /dev/null
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/RobotAutomationListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 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 org.chromium.latency.walt;
+
+public interface RobotAutomationListener {
+    public static final String START_EVENT = "START_EVENT";
+    public static final String RESTART_EVENT = "RESTART_EVENT";
+    public static final String FINISH_EVENT = "FINISH_EVENT";
+    public static final String WRITE_LOG_EVENT = "WRITE_LOG_EVENT";
+    public static final String CLEAR_LOG_EVENT = "CLEAR_LOG_EVENT";
+
+    public void onRobotAutomationEvent(String event);
+}
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java
index cfe6a53..629ed7d 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java
@@ -53,7 +53,8 @@
 /**
  * Measurement of screen response time when switching between black and white.
  */
-public class ScreenResponseFragment extends Fragment implements View.OnClickListener {
+public class ScreenResponseFragment extends Fragment
+        implements View.OnClickListener, RobotAutomationListener {
 
     private static final int CURVE_TIMEOUT = 1000;  // milliseconds
     private static final int CURVE_BLINK_TIME = 250;  // milliseconds
@@ -248,14 +249,13 @@
             Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
                 @Override
                 public void doFrame(long frameTimeNanos) {
-                    // frameTimeNanos is he time in nanoseconds when the frame started being
+                    // frameTimeNanos is the time in nanoseconds when the frame started being
                     // rendered, in the nanoTime() timebase.
                     lastFrameStartTime = frameTimeNanos / 1000 - waltDevice.clock.baseTime;
                     lastFrameCallbackTime = System.nanoTime() / 1000 - waltDevice.clock.baseTime;
                 }
             });
 
-
             // Repost doBlink to some far away time to blink again even if nothing arrives from
             // Teensy. This callback will almost always get cancelled by onIncomingTimestamp()
             handler.postDelayed(doBlinkRunnable, 550 + (long) (Math.random()*100));
@@ -290,9 +290,9 @@
                 }
             }
 
-            final long startTimeMicros = lastFrameStartTime + waltDevice.clock.baseTime;
-            final long finishTimeMicros = tmsg.t + waltDevice.clock.baseTime;
             if (traceLogger != null) {
+                final long startTimeMicros = lastFrameStartTime + waltDevice.clock.baseTime;
+                final long finishTimeMicros = tmsg.t + waltDevice.clock.baseTime;
                 traceLogger.log(startTimeMicros, finishTimeMicros,
                         isBoxWhite ? "Black-to-white" : "White-to-black",
                         "Bar starts at beginning of frame and ends when photosensor detects blink");
@@ -423,6 +423,18 @@
         }
     }
 
+    public void onRobotAutomationEvent(String event) {
+        // Never show the latency chart during automated runs.
+        shouldShowLatencyChart = false;
+        // Disable full-screen mode to prevent modal user dialog.
+        enableFullScreen = false;
+        if (event.equals(RobotAutomationListener.RESTART_EVENT)) {
+            onClick(stopButton);
+        } else if (event.equals(RobotAutomationListener.START_EVENT)) {
+            onClick(startButton);
+        }
+    }
+
     private WaltDevice.TriggerHandler brightnessTriggerHandler = new WaltDevice.TriggerHandler() {
         @Override
         public void onReceive(WaltDevice.TriggerMessage tmsg) {
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java
index 64e333d..e26a328 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/TapLatencyFragment.java
@@ -36,7 +36,7 @@
 import static org.chromium.latency.walt.Utils.getBooleanPreference;
 
 public class TapLatencyFragment extends Fragment
-    implements View.OnClickListener {
+        implements View.OnClickListener, RobotAutomationListener {
 
     private static final int ACTION_DOWN_INDEX = 0;
     private static final int ACTION_UP_INDEX = 1;
@@ -193,8 +193,8 @@
         }
 
         if (dt < 0 || dt > 200) {
-            logger.log(action + " bogus kernelTime, ignored, dt=" + dt);
-            return  false;
+            logger.log(action + " bogus kernelTime=" + e.kernelTime + ", ignored, dt=" + dt);
+            return false;
         }
         return true;
     }
@@ -303,4 +303,15 @@
         }
 
     }
+
+    public void onRobotAutomationEvent(String event) {
+        // Never show the latency chart during automated runs.
+        shouldShowLatencyChart = false;
+        if (event.equals(RobotAutomationListener.RESTART_EVENT) ||
+                event.equals(RobotAutomationListener.START_EVENT)) {
+            restartMeasurement();
+        } else if (event.equals(RobotAutomationListener.FINISH_EVENT)) {
+            finishAndShowStats();
+        }
+    }
 }
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java
index 96ddcfd..8ec2cb4 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java
@@ -156,16 +156,20 @@
 
 
     private String sendReceive(char c) throws IOException {
-        connection.sendByte(c);
-        return readOne();
+        synchronized (connection) {
+            connection.sendByte(c);
+            return readOne();
+        }
     }
 
     public void sendAndFlush(char c) {
 
         try {
-            connection.sendByte(c);
-            while(connection.blockingRead(buffer) > 0) {
-                // flushing all incoming data
+            synchronized (connection) {
+                connection.sendByte(c);
+                while (connection.blockingRead(buffer) > 0) {
+                    // flushing all incoming data
+                }
             }
         } catch (Exception e) {
             logger.log("Exception in sendAndFlush: " + e.getMessage());
@@ -187,6 +191,7 @@
             throw new IOException("Unexpected response from WALT. Expected \"" + ack
                     + "\", got \"" + response + "\"");
         }
+        // Trim out the ack
         return response.substring(1).trim();
     }
 
@@ -237,6 +242,10 @@
             return;
         }
         connection.updateLag();
+        if (clock == null) {
+            // updateLag() will have logged a message if we get here
+            return;
+        }
         int drift = Math.abs(clock.getMeanLag());
         String msg = String.format("Remote clock delayed between %d and %d us",
                 clock.minLag, clock.maxLag);
@@ -321,11 +330,13 @@
         }
 
         void onReceiveRaw(String s) {
-            if (TriggerMessage.isTriggerString(s)) {
-                TriggerMessage tmsg = new TriggerMessage(s.substring(1).trim());
-                onReceive(tmsg);
-            } else {
-                Log.i(TAG, "Malformed trigger data: " + s);
+            for (String trigger : s.split("\n")) {
+                if (TriggerMessage.isTriggerString(trigger)) {
+                    TriggerMessage tmsg = new TriggerMessage(trigger.substring(1).trim());
+                    onReceive(tmsg);
+                } else {
+                    Log.i(TAG, "Malformed trigger data: " + s);
+                }
             }
         }
 
@@ -391,6 +402,12 @@
     }
 
     public void stopListener() {
+        // If the trigger listener is already stopped, then it is possible the listener thread is
+        // null. In that case, calling stop() followed by join() will result in a listener object
+        // that is stuck in the STOPPING state.
+        if (triggerListener.isStopped()) {
+            return;
+        }
         logger.log("Stopping Listener");
         triggerListener.stop();
         try {
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java
index ee9c143..e63f4dc 100644
--- a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltTcpConnection.java
@@ -32,8 +32,8 @@
 
 public class WaltTcpConnection implements WaltConnection {
 
-    // The local ip on ARC++ to connect to underlying ChromeOS
-    private static final String SERVER_IP = "192.168.254.1";
+    // Use a "reverse" port over adb. The server is running on the host to which we're attached.
+    private static final String SERVER_IP = "127.0.0.1";
     private static final int SERVER_PORT = 50007;
     private static final int TCP_READ_TIMEOUT_MS = 200;
 
@@ -74,6 +74,10 @@
     }
 
     public void connect() {
+        // If the singleton is already connected, do not kill the connection.
+        if (isConnected()) {
+            return;
+        }
         connectionState = Utils.ListenerState.STARTING;
         networkThread = new HandlerThread("NetworkThread");
         networkThread.start();
@@ -85,6 +89,7 @@
                 try {
                     InetAddress serverAddr = InetAddress.getByName(SERVER_IP);
                     socket = new Socket(serverAddr, SERVER_PORT);
+                    socket.setKeepAlive(true);
                     socket.setSoTimeout(TCP_READ_TIMEOUT_MS);
                     outputStream = socket.getOutputStream();
                     inputStream = socket.getInputStream();
@@ -119,18 +124,39 @@
         return connectionState == Utils.ListenerState.RUNNING;
     }
 
-    public void sendByte(char c) throws IOException {
-        outputStream.write(Utils.char2byte(c));
+    public void sendByte(final char c) throws IOException {
+        // All network accesses must occur on a separate thread.
+        networkHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    outputStream.write(Utils.char2byte(c));
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
     }
 
-    public void sendString(String s) throws IOException {
-        outputStream.write(s.getBytes("UTF-8"));
+    public void sendString(final String s) throws IOException {
+        // All network accesses must occur on a separate thread.
+        networkHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    outputStream.write(s.getBytes("UTF-8"));
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
     }
 
     public synchronized int blockingRead(byte[] buff) {
 
         messageReceived = false;
 
+        // All network accesses must occur on a separate thread.
         networkHandler.post(new Runnable() {
             @Override
             public void run() {
@@ -171,8 +197,7 @@
         return lastRetVal;
     }
 
-
-    private void updateClock(String cmd) throws IOException {
+    private synchronized void updateClock(String cmd) throws IOException {
         sendString(cmd);
         int retval = blockingRead(buffer);
         if (retval <= 0) {
@@ -181,8 +206,9 @@
         String s = new String(buffer, 0, retval);
         String[] parts = s.trim().split("\\s+");
         // TODO: make sure reply starts with "clock"
-        long wallBaseTime = Long.parseLong(parts[1]);
-        remoteClock.baseTime = wallBaseTime - RemoteClockInfo.uptimeZero();
+        // The bridge sends the time difference between when it sent the reply and when it zeroed
+        // the WALT's clock. We assume here that the reply transit time is negligible.
+        remoteClock.baseTime = RemoteClockInfo.microTime() - Long.parseLong(parts[1]);
         remoteClock.minLag = Integer.parseInt(parts[2]);
         remoteClock.maxLag = Integer.parseInt(parts[3]);
     }
diff --git a/arduino/walt/walt.ino b/arduino/walt/walt.ino
index e63da3b..ddb7c95 100644
--- a/arduino/walt/walt.ino
+++ b/arduino/walt/walt.ino
@@ -68,8 +68,11 @@
 #define MIC_PIN 23        // Same as A9
 
 // Threshold and hysteresis for screen on/off reading
-#define SCREEN_THRESH_HIGH  110
-#define SCREEN_THRESH_LOW  90
+#define SCREEN_THRESH_HIGH  800
+#define SCREEN_THRESH_LOW   300
+
+// Shock threshold
+#define GSHOCK_THRESHOLD    500
 
 elapsedMicros time_us;
 
@@ -378,7 +381,7 @@
   // Probe the accelerometer
   if (gshock.probe) {
     int v = analogRead(G_PIN);
-    if (v > 900) {
+    if (v > GSHOCK_THRESHOLD) {
       gshock.t = time_us;
       gshock.count++;
       gshock.probe = false;
diff --git a/pywalt/pywalt/minimization.py b/pywalt/pywalt/minimization.py
index 1337587..dc5c0be 100644
--- a/pywalt/pywalt/minimization.py
+++ b/pywalt/pywalt/minimization.py
@@ -21,7 +21,7 @@
 """
 
 import numpy
-from . import evparser
+import evparser
 
 debug_mode = False
 
diff --git a/pywalt/pywalt/walt.py b/pywalt/pywalt/walt.py
index 2dbcf8b..9987f4f 100644
--- a/pywalt/pywalt/walt.py
+++ b/pywalt/pywalt/walt.py
@@ -48,9 +48,9 @@
 import serial
 import numpy
 
-from . import evparser
-from . import minimization
-from . import screen_stats
+import evparser
+import minimization
+import screen_stats
 
 
 # Time units
@@ -620,14 +620,31 @@
         # Discard any empty data
         if not data or len(data) == 0:
             print('o<: discarded empty data')
-            return None
+            return
 
         # Get a string version of the data for checking longer commands
         s = data.decode(self.walt.encoding)
-        if s.startswith('bridge'):
-            log('bridge command: %s, pausing ser2net thread...' % s)
+        bridge_command = None
+        while len(s) > 0:
+            if not bridge_command:
+                bridge_command = re.search(r'bridge (sync|update)', s)
+            # If a "bridge" command does not exist, send everything to the WALT
+            if not bridge_command:
+                self.walt.ser.write(s.encode(self.walt.encoding))
+                break
+            # If a "bridge" command is preceded by WALT commands, send those
+            # first
+            if bridge_command.start() > 0:
+                before_command = s[:bridge_command.start()]
+                log('found bridge command after "%s"' % before_command)
+                s = s[bridge_command.start():]
+                self.walt.ser.write(before_command.encode(self.walt.encoding))
+                continue
+            # Otherwise, reply directly to the command
+            log('bridge command: %s, pausing ser2net thread...' %
+                    bridge_command.group(0))
             self.pause()
-            is_sync = 'sync' in s
+            is_sync = bridge_command.group(1) == 'sync' or not self.walt.base_time
             if is_sync:
                 self.walt.zero_clock()
 
@@ -638,16 +655,16 @@
                 self.walt.max_lag -= self.walt.min_lag
                 self.walt.min_lag = 0
 
-            t0 = self.walt.base_time * 1e6
             min_lag = self.walt.min_lag * 1e6
             max_lag = self.walt.max_lag * 1e6
-            reply = 'clock %d %d %d\n' % (t0, min_lag, max_lag)
-            print('|custom-reply>: ' + repr(reply))
+            # Send the time difference between now and when the clock was zeroed
+            dt0 = (time.time() - self.walt.base_time) * 1e6
+            reply = 'clock %d %d %d\n' % (dt0, min_lag, max_lag)
             self.net.sendall(reply)
+            print('|custom-reply>: ' + repr(reply))
             self.resume()
-            return None
-
-        return data
+            s = s[bridge_command.end():]
+            bridge_command = None
 
     def connections_loop(self):
         with contextlib.closing(socket.socket(
@@ -655,6 +672,7 @@
             self.sock = sock
             # SO_REUSEADDR is supposed to prevent the "Address already in use" error
             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
             sock.bind((self.host, self.port))
             sock.listen(1)
             while True:
@@ -677,10 +695,7 @@
             data = self.net.recv(1024)
             if not data:
                 break  # got disconnected
-
-            data = self.net2ser(data)
-            if(data):
-                self.walt.ser.write(data)
+            self.net2ser(data)
 
     def ser2net_loop(self):
         while True: