luma_third_party: Saving user events/screens

As users interact with remote android sessions, this CL
adds support to record all user interaction data to the db as well as
record images of the screen to disk while interactions happen.

Change-Id: I391cb7cd05555c3e38f2f9ba4671733ef3ded47f
diff --git a/crowdstf/.gitignore b/crowdstf/.gitignore
index bb12375..1366020 100644
--- a/crowdstf/.gitignore
+++ b/crowdstf/.gitignore
@@ -11,3 +11,4 @@
 /temp/
 /tmp/
 .eslintcache
+/screen_shots/
diff --git a/crowdstf/README.google b/crowdstf/README.google
index 9bb5361..aec2bc3 100644
--- a/crowdstf/README.google
+++ b/crowdstf/README.google
@@ -7,4 +7,6 @@
 Control and manage Android devices from your browser. https://openstf.io
 
 Local Modifications:
-No modifications
\ No newline at end of file
+Change 23400
+  Added screen captures save-to-disk
+  Added gesture capture persistence to db
diff --git a/crowdstf/lib/cli.js b/crowdstf/lib/cli.js
index 0ba93d9..4b5938b 100644
--- a/crowdstf/lib/cli.js
+++ b/crowdstf/lib/cli.js
@@ -8,6 +8,9 @@
 var pkg = require('../package')
 var cliutil = require('./util/cliutil')
 var logger = require('./util/logger')
+var path = require('path')
+
+global.appRoot = path.resolve(__dirname);
 
 Promise.longStackTraces()
 
diff --git a/crowdstf/lib/db/api.js b/crowdstf/lib/db/api.js
index 59c801f..5e76b78 100644
--- a/crowdstf/lib/db/api.js
+++ b/crowdstf/lib/db/api.js
@@ -141,6 +141,43 @@
     }))
 }
 
+/**
+ * Persists android device events
+ * @example
+ *   dbapi.saveDeviceEvent("foo", "bar"....)
+ *   .then(function(result) {
+ *     console.log("Saved", result.inserted, "device events");
+ *   })
+ *   .catch(function(err){
+ *     throw err;
+ *   })
+ * @returns {Promise} Returns a promise with RethinkDB result
+ */
+dbapi.saveDeviceEvent = function(deviceSerial, sessionId, eventName, imgId,
+    timestamp, seq, contact, x, y, pressure, userEmail, userGroup, userIP,
+    userLastLogin, userName) {
+  var deviceEventDTO = {
+    serial: deviceSerial,
+    sessionId: sessionId,
+    eventName: eventName,
+    imgId: imgId,
+    timestamp: timestamp,
+    seq: seq === undefined ? null : seq,
+    x: x === undefined ? null : x,
+    y: y === undefined ? null : y,
+    pressure: (pressure === undefined ? null : pressure),
+    userEmail: userEmail,
+    userGroup: userGroup,
+    userIP: userIP,
+    userLastLogin: userLastLogin,
+    userName: userName
+  };
+
+  return db.run(r.table('deviceEvents').insert(deviceEventDTO, {
+    durability: 'soft'
+  }))
+}
+
 dbapi.saveDeviceInitialState = function(serial, device) {
   var data = {
     present: false
diff --git a/crowdstf/lib/db/tables.js b/crowdstf/lib/db/tables.js
index 231f597..5dc2607 100644
--- a/crowdstf/lib/db/tables.js
+++ b/crowdstf/lib/db/tables.js
@@ -54,4 +54,7 @@
 , logs: {
     primaryKey: 'id'
   }
+, deviceEvents: {
+    primaryKey: 'id'
+  }
 }
diff --git a/crowdstf/lib/units/device/plugins/screen/stream.js b/crowdstf/lib/units/device/plugins/screen/stream.js
index 9d2c738..3e4c1fa 100644
--- a/crowdstf/lib/units/device/plugins/screen/stream.js
+++ b/crowdstf/lib/units/device/plugins/screen/stream.js
@@ -13,11 +13,15 @@
 var bannerutil = require('./util/banner')
 var FrameParser = require('./util/frameparser')
 var FrameConfig = require('./util/frameconfig')
+var FrameStore = require('./util/framestore')
 var BroadcastSet = require('./util/broadcastset')
 var StateQueue = require('../../../../util/statequeue')
 var RiskyStream = require('../../../../util/riskystream')
 var FailCounter = require('../../../../util/failcounter')
 
+const FINAL_DEVICE_CAPTURE_WIDTH = 1080;
+const FINAL_DEVICE_CAPTURE_HEIGHT = 1920;
+
 module.exports = syrup.serial()
   .dependency(require('../../support/adb'))
   .dependency(require('../../resources/minicap'))
@@ -25,6 +29,7 @@
   .dependency(require('./options'))
   .define(function(options, adb, minicap, display, screenOptions) {
     var log = logger.createLogger('device:plugins:screen:stream')
+    var frameStore = new FrameStore();
 
     function FrameProducer(config) {
       EventEmitter.call(this)
@@ -175,6 +180,22 @@
       this._configChanged()
     }
 
+    FrameProducer.prototype.setStaticProjection = function() {
+      if (this.frameConfig.virtualWidth === FINAL_DEVICE_CAPTURE_WIDTH &&
+          this.frameConfig.virtualHeight === FINAL_DEVICE_CAPTURE_HEIGHT) {
+        log.info('Keeping %dx%d as current frame producer projection',
+          FINAL_DEVICE_CAPTURE_WIDTH, FINAL_DEVICE_CAPTURE_HEIGHT)
+        return
+      }
+
+      log.info('Setting frame producer projection to %dx%d',
+        FINAL_DEVICE_CAPTURE_WIDTH,
+        FINAL_DEVICE_CAPTURE_HEIGHT)
+      this.frameConfig.virtualWidth = FINAL_DEVICE_CAPTURE_WIDTH
+      this.frameConfig.virtualHeight = FINAL_DEVICE_CAPTURE_HEIGHT
+      this._configChanged()
+    }
+
     FrameProducer.prototype.nextFrame = function() {
       var frame = null
       var chunk
@@ -483,7 +504,7 @@
           var frame = frameProducer.nextFrame()
           if (frame) {
             Promise.settle([broadcastSet.keys().map(function(id) {
-              return broadcastSet.get(id).onFrame(frame)
+              return broadcastSet.get(id).onFrame(frame, id)
             })]).then(next)
           }
           else {
@@ -501,10 +522,10 @@
 
           function wsStartNotifier() {
             return new Promise(function(resolve, reject) {
-              var message = util.format(
-                'start %s'
-              , JSON.stringify(frameProducer.banner)
-              )
+              frameProducer.banner.wsId = id;
+
+              var message = util.format('start %s',
+                JSON.stringify(frameProducer.banner))
 
               switch (ws.readyState) {
               case WebSocket.OPENING:
@@ -530,7 +551,7 @@
             })
           }
 
-          function wsFrameNotifier(frame) {
+          function wsFrameNotifier(frame, id) {
             return new Promise(function(resolve, reject) {
               switch (ws.readyState) {
               case WebSocket.OPENING:
@@ -538,6 +559,18 @@
                 return reject(new Error(util.format(
                   'Unable to send frame to OPENING client "%s"', id)))
               case WebSocket.OPEN:
+                var fileName = frameStore.storeFrame(frame, id);
+
+                var message = util.format(
+                  'nextImgId %s'
+                  , fileName
+                )
+
+                // Send the next img file id first
+                ws.send(message, function (err) {
+                  return err ? reject(err) : resolve()
+                })
+
                 // This is what SHOULD happen.
                 ws.send(frame, {
                   binary: true
@@ -572,8 +605,7 @@
                 broadcastSet.remove(id)
                 break
               case 'size':
-                frameProducer.updateProjection(
-                  Number(match[3]), Number(match[4]))
+                frameProducer.setStaticProjection()
                 break
               }
             }
diff --git a/crowdstf/lib/units/device/plugins/screen/util/framestore.js b/crowdstf/lib/units/device/plugins/screen/util/framestore.js
new file mode 100644
index 0000000..0bf4296
--- /dev/null
+++ b/crowdstf/lib/units/device/plugins/screen/util/framestore.js
@@ -0,0 +1,34 @@
+var util = require('util')
+var path = require('path')
+var fs = require('fs')
+var mkdirp = require('mkdirp')
+
+var FINAL_SCREEN_SHOT_DIR = path.join(appRoot, "/../screen_shots/")
+mkdirp.sync(FINAL_SCREEN_SHOT_DIR)
+
+function FrameStore() {
+  this.sessionImgCountMap = {};
+}
+
+FrameStore.prototype.storeFrame = function(frame, sessionId) {
+  if (this.sessionImgCountMap[sessionId]) {
+    this.sessionImgCountMap[sessionId] += 1;
+  } else {
+    this.sessionImgCountMap[sessionId] = 1;
+  }
+
+  var sessionImgNumber = this.sessionImgCountMap[sessionId];
+  var fileName = util.format('%s_%s_%d.jpg', sessionId, sessionImgNumber,
+    Date.now());
+  var filePath = path.join(FINAL_SCREEN_SHOT_DIR, fileName)
+
+  fs.writeFile(filePath, frame, function(err) {
+    if (err) {
+      console.log(err);
+    }
+  });
+
+  return fileName;
+}
+
+module.exports = FrameStore
diff --git a/crowdstf/lib/units/device/plugins/vnc/index.js b/crowdstf/lib/units/device/plugins/vnc/index.js
index e6a6fd2..e0a79cc 100644
--- a/crowdstf/lib/units/device/plugins/vnc/index.js
+++ b/crowdstf/lib/units/device/plugins/vnc/index.js
@@ -225,8 +225,7 @@
           }
 
           conn.on('authenticated', function() {
-            screenStream.updateProjection(
-              options.vncInitialSize[0], options.vncInitialSize[1])
+            screenStream.setStaticProjection()
             screenStream.broadcastSet.insert(id, {
               onStart: vncStartListener
             , onFrame: vncFrameListener
diff --git a/crowdstf/lib/units/websocket/deviceeventstore.js b/crowdstf/lib/units/websocket/deviceeventstore.js
new file mode 100644
index 0000000..5d597cd
--- /dev/null
+++ b/crowdstf/lib/units/websocket/deviceeventstore.js
@@ -0,0 +1,22 @@
+var dbapi = require('../../db/api')
+
+function DeviceEventStore() {
+}
+
+DeviceEventStore.prototype.storeEvent = function (eventName, eventData) {
+  var imgId = eventData.imgId;
+  var serial = eventData.serial;
+  var timestamp = eventData.timestamp;
+  var sessionId = eventData.wsId;
+  var userEmail = eventData.userEmail;
+  var userGroup = eventData.userGroup;
+  var userIP = eventData.userIP;
+  var userLastLogin = eventData.userLastLogin;
+  var userName = eventData.userName
+
+  dbapi.saveDeviceEvent(serial, sessionId, eventName, imgId, timestamp,
+      eventData.seq, eventData.contact, eventData.x, eventData.y, eventData.pressure,
+      userEmail, userGroup, userIP, userLastLogin, userName)
+}
+
+module.exports = DeviceEventStore
diff --git a/crowdstf/lib/units/websocket/index.js b/crowdstf/lib/units/websocket/index.js
index a04e388..e3e25f0 100644
--- a/crowdstf/lib/units/websocket/index.js
+++ b/crowdstf/lib/units/websocket/index.js
@@ -22,6 +22,10 @@
 var ip = require('./middleware/remote-ip')
 var auth = require('./middleware/auth')
 var jwtutil = require('../../util/jwtutil')
+var DeviceEventStore = require('./deviceeventstore')
+var deviceEventStore = new DeviceEventStore();
+var layoutCaptureService = require('./layoutcaptureservice')
+
 
 module.exports = function(options) {
   var log = logger.createLogger('websocket')
@@ -441,110 +445,158 @@
         })
         // Touch events
         .on('input.touchDown', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.TouchDownMessage(
-              data.seq
-            , data.contact
-            , data.x
-            , data.y
-            , data.pressure
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.TouchDownMessage, function() {
+            deviceEventStore.storeEvent('input.touchDown', data);
+            push.send([
+              channel
+              , wireutil.envelope(new wire.TouchDownMessage(
+                data.seq
+                , data.contact
+                , data.x
+                , data.y
+                , data.pressure
+              ))
+            ])
+          });
         })
         .on('input.touchMove', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.TouchMoveMessage(
-              data.seq
-            , data.contact
-            , data.x
-            , data.y
-            , data.pressure
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.TouchMoveMessage, function() {
+            deviceEventStore.storeEvent('input.touchMove', data);
+            push.send([
+              channel
+              , wireutil.envelope(new wire.TouchMoveMessage(
+                data.seq
+                , data.contact
+                , data.x
+                , data.y
+                , data.pressure
+              ))
+            ])
+          });
         })
         .on('input.touchUp', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.TouchUpMessage(
-              data.seq
-            , data.contact
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.TouchUpMessage, function() {
+            deviceEventStore.storeEvent('input.touchUp', data);
+
+            push.send([
+              channel
+              , wireutil.envelope(new wire.TouchUpMessage(
+                data.seq
+                , data.contact
+              ))
+            ])
+          });
+
         })
         .on('input.touchCommit', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.TouchCommitMessage(
-              data.seq
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.TouchCommitMessage, function() {
+            deviceEventStore.storeEvent('input.touchCommit', data);
+
+            push.send([
+              channel
+              , wireutil.envelope(new wire.TouchCommitMessage(
+                data.seq
+              ))
+            ])
+          });
+
         })
         .on('input.touchReset', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.TouchResetMessage(
-              data.seq
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.TouchResetMessage, function() {
+            deviceEventStore.storeEvent('input.touchReset', data);
+
+            push.send([
+              channel
+              , wireutil.envelope(new wire.TouchResetMessage(
+                data.seq
+              ))
+            ])
+          });
+
         })
         .on('input.gestureStart', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.GestureStartMessage(
-              data.seq
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.GestureStartMessage, function(xmlRes) {
+            console.log("Received XML:", xmlRes)
+
+            data.xml = xmlRes;
+            deviceEventStore.storeEvent('input.gestureStart', data);
+
+            push.send([
+              channel
+              , wireutil.envelope(new wire.GestureStartMessage(
+                data.seq
+              ))
+            ])
+          });
+
         })
         .on('input.gestureStop', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.GestureStopMessage(
-              data.seq
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.GestureStopMessage, function() {
+            deviceEventStore.storeEvent('input.gestureStop', data);
+
+            push.send([
+              channel
+              , wireutil.envelope(new wire.GestureStopMessage(
+                data.seq
+              ))
+            ])
+          });
+
         })
         // Key events
         .on('input.keyDown', createKeyHandler(wire.KeyDownMessage))
         .on('input.keyUp', createKeyHandler(wire.KeyUpMessage))
         .on('input.keyPress', createKeyHandler(wire.KeyPressMessage))
         .on('input.type', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.TypeMessage(
-              data.text
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.TypeMessage, function() {
+            deviceEventStore.storeEvent('input.keyPress', data);
+
+            push.send([
+              channel
+              , wireutil.envelope(new wire.TypeMessage(
+                data.text
+              ))
+            ])
+          });
         })
         .on('display.rotate', function(channel, data) {
-          push.send([
-            channel
-          , wireutil.envelope(new wire.RotateMessage(
-              data.rotation
-            ))
-          ])
+          layoutCaptureService.enqueue(wire.RotateMessage, function() {
+            push.send([
+              channel
+              , wireutil.envelope(new wire.RotateMessage(
+                data.rotation
+              ))
+            ])
+          })
         })
         // Transactions
         .on('clipboard.paste', function(channel, responseChannel, data) {
-          joinChannel(responseChannel)
-          push.send([
-            channel
-          , wireutil.transaction(
-              responseChannel
-            , new wire.PasteMessage(data.text)
-            )
-          ])
+          layoutCaptureService.enqueue(wire.PasteMessage, function() {
+            deviceEventStore.storeEvent('clipboard.paste', data);
+
+            joinChannel(responseChannel)
+            push.send([
+              channel
+              , wireutil.transaction(
+                responseChannel
+                , new wire.PasteMessage(data.text)
+              )
+            ])
+          });
         })
         .on('clipboard.copy', function(channel, responseChannel) {
-          joinChannel(responseChannel)
-          push.send([
-            channel
-          , wireutil.transaction(
-              responseChannel
-            , new wire.CopyMessage()
-            )
-          ])
+          layoutCaptureService.enqueue(wire.CopyMessage, function() {
+            deviceEventStore.storeEvent('clipboard.copy', {});
+
+            joinChannel(responseChannel)
+            push.send([
+              channel
+              , wireutil.transaction(
+                responseChannel
+                , new wire.CopyMessage()
+              )
+            ])
+          });
         })
         .on('device.identify', function(channel, responseChannel) {
           push.send([
diff --git a/crowdstf/lib/units/websocket/layoutcaptureservice.js b/crowdstf/lib/units/websocket/layoutcaptureservice.js
new file mode 100644
index 0000000..8ad7696
--- /dev/null
+++ b/crowdstf/lib/units/websocket/layoutcaptureservice.js
@@ -0,0 +1,70 @@
+var wire = require('../../wire')
+
+function LayoutCaptureService() {
+  this.actionQueue = [];
+}
+
+LayoutCaptureService.prototype.enqueue = function(wireEvent, actionFn) {
+  this.actionQueue.push({
+    wireEvent: wireEvent,
+    actionFn: actionFn
+  });
+  this.checkStartCaptures(wireEvent);
+}
+
+LayoutCaptureService.prototype.dequeue = function() {
+  if (this.actionQueue.length > 0) {
+    return this.actionQueue.shift();
+  } else {
+    return null;
+  }
+}
+
+LayoutCaptureService.prototype.checkStartCaptures = function() {
+  if (this.actionQueue.length > 0 && !this.processing) {
+    this.processing = true;
+    this.processStr = "";
+    var nextItem = function() {
+      var eventActionObj = layoutCaptureService.dequeue();
+      if (eventActionObj) {
+        layoutCaptureService.processStr += " (" +
+          eventActionObj.wireEvent.$code + ") ";
+        if (eventActionObj.wireEvent === wire.GestureStartMessage) {
+          console.log("Queueing gesture-start event")
+
+          layoutCaptureService.fetchLayout(function(err, res) {
+            if (err) {
+              console.error(err);
+            } else {
+              eventActionObj.actionFn(res)
+              nextItem()
+            }
+          });
+        } else {
+          if (eventActionObj.wireEvent === wire.GestureStopMessage) {
+            console.log("Queueing gesture-stop event")
+          }
+
+          eventActionObj.actionFn()
+          nextItem()
+        }
+      } else {
+        layoutCaptureService.processing = false;
+      }
+    }
+
+    nextItem();
+  }
+}
+
+LayoutCaptureService.prototype.fetchLayout = function(callback) {
+  //TODO(hibschman@): swap out this delay simulation stub with Device XML Fetch
+  var rand = Math.floor(Math.random() * (300 - 100 + 1) + 100);
+  console.log("Delay", rand, "millis")
+  setTimeout(function() {
+    callback(null, "<xml layout='mock'></xml>");
+  }, rand)
+};
+
+var layoutCaptureService = new LayoutCaptureService();
+module.exports = layoutCaptureService;
diff --git a/crowdstf/lib/util/procutil.js b/crowdstf/lib/util/procutil.js
index 25421f1..eff1f60 100644
--- a/crowdstf/lib/util/procutil.js
+++ b/crowdstf/lib/util/procutil.js
@@ -23,6 +23,7 @@
   log.info('Forking "%s %s"', filename, args.join(' '))
 
   var resolver = Promise.defer()
+
   var proc = cp.fork.apply(cp, arguments)
 
   function sigintListener() {
diff --git a/crowdstf/package.json b/crowdstf/package.json
index 053aa2e..e38cf86 100644
--- a/crowdstf/package.json
+++ b/crowdstf/package.json
@@ -60,17 +60,18 @@
     "markdown-serve": "^0.3.2",
     "mime": "^1.3.4",
     "minimatch": "^3.0.0",
+    "mkdirp": "^0.5.1",
     "my-local-ip": "^1.0.0",
     "node-uuid": "^1.4.3",
-    "passport": "^0.3.2",
     "openid": "^0.5.13",
+    "passport": "^0.3.2",
     "passport-oauth2": "^1.1.2",
     "passport-saml": "^0.15.0",
     "protobufjs": "^3.8.2",
     "proxy-addr": "^1.0.10",
     "request": "^2.67.0",
     "request-progress": "^2.0.1",
-    "rethinkdb": "^2.0.2",
+    "rethinkdb": "^2.3.1",
     "semver": "^5.0.1",
     "serve-favicon": "^2.2.0",
     "serve-static": "^1.9.2",
@@ -84,9 +85,9 @@
     "stf-wiki": "^1.0.0",
     "temp": "^0.8.1",
     "transliteration": "^0.1.1",
+    "url-join": "0.0.1",
     "utf-8-validate": "^1.2.1",
     "ws": "^1.0.1",
-    "url-join": "0.0.1",
     "zmq": "^2.14.0"
   },
   "devDependencies": {
diff --git a/crowdstf/res/app/components/stf/control/control-service.js b/crowdstf/res/app/components/stf/control/control-service.js
index 5246860..cf5446d 100644
--- a/crowdstf/res/app/components/stf/control/control-service.js
+++ b/crowdstf/res/app/components/stf/control/control-service.js
@@ -1,3 +1,5 @@
+var imageFile = require('../screen/imagefile')
+
 module.exports = function ControlServiceFactory(
   $upload
 , $http
@@ -6,12 +8,22 @@
 , $rootScope
 , gettext
 , KeycodesMapped
+, UserService
 ) {
   var controlService = {
   }
 
   function ControlService(target, channel) {
     function sendOneWay(action, data) {
+      data.imgId = imageFile.getNextImgId();
+      data.serial = imageFile.getCurrentDeviceSerial();
+      data.timestamp = new Date().getTime();
+      data.wsId = socket.getWsId();
+      data.userEmail = UserService.currentUser.email;
+      data.userGroup = UserService.currentUser.group;
+      data.userIP = UserService.currentUser.ip;
+      data.userLastLogin = UserService.currentUser.lastLoggedInAt;
+      data.userName = UserService.currentUser.name;
       socket.emit(action, channel, data)
     }
 
diff --git a/crowdstf/res/app/components/stf/screen/imagefile.js b/crowdstf/res/app/components/stf/screen/imagefile.js
new file mode 100644
index 0000000..04ce7aa
--- /dev/null
+++ b/crowdstf/res/app/components/stf/screen/imagefile.js
@@ -0,0 +1,22 @@
+function ImageFile() {
+  this.nextImgId = ""
+  this.deviceSerial = ""
+}
+
+ImageFile.prototype.getNextImgId = function() {
+  return this.nextImgId;
+}
+
+ImageFile.prototype.setNextImgId = function(nextImgId) {
+  this.nextImgId = nextImgId;
+}
+
+ImageFile.prototype.getCurrentDeviceSerial = function() {
+  return this.deviceSerial;
+}
+
+ImageFile.prototype.setCurrentDeviceSerial = function(deviceSerial) {
+  this.deviceSerial = deviceSerial;
+}
+
+module.exports = new ImageFile()
diff --git a/crowdstf/res/app/components/stf/screen/screen-directive.js b/crowdstf/res/app/components/stf/screen/screen-directive.js
index ec4e56d..8d6e2d2 100644
--- a/crowdstf/res/app/components/stf/screen/screen-directive.js
+++ b/crowdstf/res/app/components/stf/screen/screen-directive.js
@@ -1,6 +1,7 @@
 var _ = require('lodash')
 var rotator = require('./rotator')
 var ImagePool = require('./imagepool')
+var imageFile = require('./imagefile')
 
 module.exports = function DeviceScreenDirective(
   $document
@@ -9,6 +10,7 @@
 , PageVisibilityService
 , $timeout
 , $window
+, socket
 ) {
   return {
     restrict: 'E'
@@ -340,7 +342,26 @@
               }
             }
             else if (/^start /.test(message.data)) {
-              applyQuirks(JSON.parse(message.data.substr('start '.length)))
+              var banner = {};
+
+              try{
+                banner = JSON.parse(message.data.substr('start '.length));
+              }catch(err){
+                // This shouldn't happen, but if it does, return early
+                // to avoid breaking the message queue and log the error
+                console.error('Invalid JSON in response', err.stack)
+                return;
+              }
+
+              var wsId = banner.wsId;
+              socket.setWSId(wsId);
+
+              applyQuirks(banner)
+            }
+            else if (/^nextImgId /.test(message.data)) {
+              var nextImgId = message.data.substr('nextImgId '.length);
+              imageFile.setNextImgId(nextImgId);
+              imageFile.setCurrentDeviceSerial(device.serial)
             }
             else if (message.data === 'secure_on') {
               scope.$apply(function() {
diff --git a/crowdstf/res/app/components/stf/socket/socket-service.js b/crowdstf/res/app/components/stf/socket/socket-service.js
index e11d82b..a16f706 100644
--- a/crowdstf/res/app/components/stf/socket/socket-service.js
+++ b/crowdstf/res/app/components/stf/socket/socket-service.js
@@ -11,6 +11,14 @@
     reconnection: false, transports: ['websocket']
   })
 
+  socket.setWSId = function(wsId){
+    this.wsId = wsId;
+  }
+
+  socket.getWsId = function(){
+    return this.wsId;
+  }
+
   socket.scoped = function($scope) {
     var listeners = []