Bug 1090209 - Part 2 Use MozLoopService to manage window ids centrally, and store the data for the window opening. r=mikedeboer a=loop-only
authorMark Banner <standard8@mozilla.com>
Mon, 03 Nov 2014 16:34:03 +0000
changeset 235110 fa172bfb0bfc1b5bc73996637f18e027c40c0ae9
parent 235109 6fd86797e66d1290c1cc2dde276225018959c48b
child 235111 a279b0202fe445e1e1301f013d3096218cc8916f
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer, loop-only
bugs1090209
milestone35.0a2
Bug 1090209 - Part 2 Use MozLoopService to manage window ids centrally, and store the data for the window opening. r=mikedeboer a=loop-only Use LoopCalls directly to handle busy statuses.
browser/components/loop/LoopCalls.jsm
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/conversationAppStore.js
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/conversationStore.js
browser/components/loop/content/shared/js/localRoomStore.js
browser/components/loop/test/desktop-local/conversationAppStore_test.js
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/shared/conversationStore_test.js
browser/components/loop/test/shared/localRoomStore_test.js
browser/components/loop/test/xpcshell/test_loopservice_busy.js
browser/components/loop/test/xpcshell/test_loopservice_directcall.js
--- a/browser/components/loop/LoopCalls.jsm
+++ b/browser/components/loop/LoopCalls.jsm
@@ -42,17 +42,17 @@ CallProgressSocket.prototype = {
    *
    * @param {function} Callback used after a successful handshake
    *                   over the progressUrl.
    * @param {function} Callback used if an error is encountered
    */
   connect: function(onSuccess, onError) {
     this._onSuccess = onSuccess;
     this._onError = onError ||
-      (reason => {MozLoopService.logwarn("LoopCalls::callProgessSocket - ", reason);});
+      (reason => {MozLoopService.log.warn("LoopCalls::callProgessSocket - ", reason);});
 
     if (!onSuccess) {
       this._onError("missing onSuccess argument");
       return;
     }
 
     if (Services.io.offline) {
       this._onError("IO offline");
@@ -121,19 +121,18 @@ CallProgressSocket.prototype = {
    *
    * @param {nsISupports} aContext Not used
    * @param {String} aMsg The message data
    */
   onMessageAvailable: function(aContext, aMsg) {
     let msg = {};
     try {
       msg = JSON.parse(aMsg);
-    }
-    catch (error) {
-      MozLoopService.logerror("LoopCalls: error parsing progress message - ", error);
+    } catch (error) {
+      MozLoopService.log.error("LoopCalls: error parsing progress message - ", error);
       return;
     }
 
     if (msg.messageType && msg.messageType === 'hello') {
       this._handshakeComplete = true;
       this._onSuccess();
     }
   },
@@ -141,17 +140,17 @@ CallProgressSocket.prototype = {
 
   /**
    * Create a JSON message payload and send on websocket.
    *
    * @param {Object} aMsg Message to send.
    */
   _send: function(aMsg) {
     if (!this._handshakeComplete) {
-      MozLoopService.logwarn("LoopCalls::_send error - handshake not complete");
+      MozLoopService.log.warn("LoopCalls::_send error - handshake not complete");
       return;
     }
 
     try {
       this._websocket.sendMsg(JSON.stringify(aMsg));
     }
     catch (error) {
       this._onError(error);
@@ -174,24 +173,22 @@ CallProgressSocket.prototype = {
 /**
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
 let LoopCallsInternal = {
-  callsData: {
-    inUse: false,
-  },
-
   mocks: {
     webSocket: undefined,
   },
 
+  conversationInProgress: {},
+
   /**
    * Callback from MozLoopPushHandler - A push notification has been received from
    * the server.
    *
    * @param {String} version The version information from the server.
    */
   onNotification: function(version, channelID) {
     if (MozLoopService.doNotDisturb) {
@@ -243,67 +240,58 @@ let LoopCallsInternal = {
    *
    */
 
   _processCalls: function(response, sessionType) {
     try {
       let respData = JSON.parse(response.body);
       if (respData.calls && Array.isArray(respData.calls)) {
         respData.calls.forEach((callData) => {
-          if (!this.callsData.inUse) {
+          if ("id" in this.conversationInProgress) {
+            this._returnBusy(callData);
+          } else {
             callData.sessionType = sessionType;
-            // XXX Bug 1090209 will transiton into a better window id.
-            callData.windowId = callData.callId;
             callData.type = "incoming";
             this._startCall(callData);
-          } else {
-            this._returnBusy(callData);
           }
         });
       } else {
-        MozLoopService.logwarn("Error: missing calls[] in response");
+        MozLoopService.log.warn("Error: missing calls[] in response");
       }
     } catch (err) {
-      MozLoopService.logwarn("Error parsing calls info", err);
+      MozLoopService.log.warn("Error parsing calls info", err);
     }
   },
 
   /**
    * Starts a call, saves the call data, and opens a chat window.
    *
    * @param {Object} callData The data associated with the call including an id.
    *                          The data should include the type - "incoming" or
    *                          "outgoing".
    */
   _startCall: function(callData) {
-    this.callsData.inUse = true;
-    this.callsData.data = callData;
-    MozLoopService.openChatWindow(
-      null,
-      // No title, let the page set that, to avoid flickering.
-      "",
-      "about:loopconversation#" + callData.windowId);
+    this.conversationInProgress.id = MozLoopService.openChatWindow(callData);
   },
 
   /**
    * Starts a direct call to the contact addresses.
    *
    * @param {Object} contact The contact to call
    * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
    * @return true if the call is opened, false if it is not opened (i.e. busy)
    */
   startDirectCall: function(contact, callType) {
-    if (this.callsData.inUse)
+    if ("id" in this.conversationInProgress)
       return false;
 
     var callData = {
       contact: contact,
       callType: callType,
-      type: "outgoing",
-      windowId: Math.floor((Math.random() * 100000000))
+      type: "outgoing"
     };
 
     this._startCall(callData);
     return true;
   },
 
    /**
    * Open call progress websocket and terminate with a reason of busy
@@ -337,45 +325,41 @@ this.LoopCalls = {
    *
    * @param {String} version The version information from the server.
    */
   onNotification: function(version, channelID) {
     LoopCallsInternal.onNotification(version, channelID);
   },
 
   /**
-   * Returns the callData for a specific conversation window id.
+   * Used to signify that a call is in progress.
    *
-   * The data was retrieved from the LoopServer via a GET/calls/<version> request
-   * triggered by an incoming message from the LoopPushServer.
-   *
-   * @param {Number} conversationWindowId
-   * @return {callData} The callData or undefined if error.
+   * @param {String} The window id for the call in progress.
    */
-  getCallData: function(conversationWindowId) {
-    if (LoopCallsInternal.callsData.data &&
-        LoopCallsInternal.callsData.data.windowId == conversationWindowId) {
-      return LoopCallsInternal.callsData.data;
-    } else {
-      return undefined;
+  setCallInProgress: function(conversationWindowId) {
+    if ("id" in LoopCallsInternal.conversationInProgress &&
+        LoopCallsInternal.conversationInProgress.id != conversationWindowId) {
+      MozLoopService.log.error("Starting a new conversation when one is already in progress?");
+      return;
     }
+
+    LoopCallsInternal.conversationInProgress.id = conversationWindowId;
   },
 
   /**
    * Releases the callData for a specific conversation window id.
    *
    * The result of this call will be a free call session slot.
    *
    * @param {Number} conversationWindowId
    */
-  releaseCallData: function(conversationWindowId) {
-    if (LoopCallsInternal.callsData.data &&
-        LoopCallsInternal.callsData.data.windowId == conversationWindowId) {
-      LoopCallsInternal.callsData.data = undefined;
-      LoopCallsInternal.callsData.inUse = false;
+  clearCallInProgress: function(conversationWindowId) {
+    if ("id" in LoopCallsInternal.conversationInProgress &&
+        LoopCallsInternal.conversationInProgress.id == conversationWindowId) {
+      delete LoopCallsInternal.conversationInProgress.id;
     }
   },
 
     /**
      * Starts a direct call to the contact addresses.
      *
      * @param {Object} contact The contact to call
      * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -99,20 +99,31 @@ const cloneValueInto = function(value, t
  * @param {nsIDOMWindow} targetWindow The content window to attach the API
  */
 const injectObjectAPI = function(api, targetWindow) {
   let injectedAPI = {};
   // Wrap all the methods in `api` to help results passed to callbacks get
   // through the priv => unpriv barrier with `Cu.cloneInto()`.
   Object.keys(api).forEach(func => {
     injectedAPI[func] = function(...params) {
-      let callback = params.pop();
-      api[func](...params, function(...results) {
-        callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
-      });
+      let lastParam = params.pop();
+
+      // If the last parameter is a function, assume its a callback
+      // and wrap it differently.
+      if (lastParam && typeof lastParam === "function") {
+        api[func](...params, function(...results) {
+          lastParam(...[cloneValueInto(r, targetWindow) for (r of results)]);
+        });
+      } else {
+        try {
+          return cloneValueInto(api[func](...params, lastParam), targetWindow);
+        } catch (ex) {
+          return cloneValueInto(ex, targetWindow);
+        }
+      }
     };
   });
 
   let contentObj = Cu.cloneInto(injectedAPI, targetWindow, {cloneFunctions: true});
   // Since we deny preventExtensions on XrayWrappers, because Xray semantics make
   // it difficult to act like an object has actually been frozen, we try to seal
   // the `contentObj` without Xrays.
   try {
@@ -130,16 +141,17 @@ const injectObjectAPI = function(api, ta
  * @param {nsIDOMWindow} targetWindow The content window to attach the API.
  */
 function injectLoopAPI(targetWindow) {
   let ringer;
   let ringerStopper;
   let appVersionInfo;
   let contactsAPI;
   let roomsAPI;
+  let callsAPI;
 
   let api = {
     /**
      * Gets an object with data that represents the currently
      * authenticated user's identity.
      *
      * @return null if user not logged in; profile object otherwise
      */
@@ -201,44 +213,30 @@ function injectLoopAPI(targetWindow) {
     locale: {
       enumerable: true,
       get: function() {
         return MozLoopService.locale;
       }
     },
 
     /**
-     * Returns the callData for a specific conversation window id.
+     * Returns the window data for a specific conversation window id.
      *
-     * The data was retrieved from the LoopServer via a GET/calls/<version> request
-     * triggered by an incoming message from the LoopPushServer.
+     * This data will be relevant to the type of window, e.g. rooms or calls.
+     * See LoopRooms or LoopCalls for more information.
      *
-     * @param {Number} conversationWindowId
-     * @returns {callData} The callData or undefined if error.
+     * @param {String} conversationWindowId
+     * @returns {Object} The window data or null if error.
      */
-    getCallData: {
+    getConversationWindowData: {
       enumerable: true,
       writable: true,
       value: function(conversationWindowId) {
-        return Cu.cloneInto(LoopCalls.getCallData(conversationWindowId), targetWindow);
-      }
-    },
-
-    /**
-     * Releases the callData for a specific conversation window id.
-     *
-     * The result of this call will be a free call session slot.
-     *
-     * @param {Number} conversationWindowId
-     */
-    releaseCallData: {
-      enumerable: true,
-      writable: true,
-      value: function(conversationWindowId) {
-        LoopCalls.releaseCallData(conversationWindowId);
+        return Cu.cloneInto(MozLoopService.getConversationWindowData(conversationWindowId),
+          targetWindow);
       }
     },
 
     /**
      * Returns the contacts API.
      *
      * @returns {Object} The contacts API object
      */
@@ -269,16 +267,32 @@ function injectLoopAPI(targetWindow) {
         if (roomsAPI) {
           return roomsAPI;
         }
         return roomsAPI = injectObjectAPI(LoopRooms, targetWindow);
       }
     },
 
     /**
+     * Returns the calls API.
+     *
+     * @returns {Object} The rooms API object
+     */
+    calls: {
+      enumerable: true,
+      get: function() {
+        if (callsAPI) {
+          return callsAPI;
+        }
+
+        return callsAPI = injectObjectAPI(LoopCalls, targetWindow);
+      }
+    },
+
+    /**
      * Import a list of (new) contacts from an external data source.
      *
      * @param {Object}   options  Property bag of options for the importer
      * @param {Function} callback Function that will be invoked once the operation
      *                            finished. The first argument passed will be an
      *                            `Error` object or `null`. The second argument will
      *                            be the result of the operation, if successfull.
      */
@@ -665,31 +679,16 @@ function injectLoopAPI(targetWindow) {
      */
     generateUUID: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.generateUUID();
       }
     },
-
-    /**
-     * Starts a direct call to the contact addresses.
-     *
-     * @param {Object} contact The contact to call
-     * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
-     * @return true if the call is opened, false if it is not opened (i.e. busy)
-     */
-    startDirectCall: {
-      enumerable: true,
-      writable: true,
-      value: function(contact, callType) {
-        LoopCalls.startDirectCall(contact, callType);
-      }
-    },
   };
 
   function onStatusChanged(aSubject, aTopic, aData) {
     let event = new targetWindow.CustomEvent("LoopStatusChanged");
     targetWindow.dispatchEvent(event);
   };
 
   function onDOMWindowDestroyed(aSubject, aTopic, aData) {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -111,16 +111,18 @@ function getJSONPref(aName) {
 // unsuccessful.
 let gRegisteredDeferred = null;
 let gHawkClient = null;
 let gLocalizedStrings = null;
 let gFxAEnabled = true;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
 let gErrors = new Map();
+let gLastWindowId = 0;
+let gConversationWindowData = new Map();
 
 /**
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
@@ -688,25 +690,30 @@ let MozLoopServiceInternal = {
         worker.postMessage(job);
       });
     }, pc.id);
   },
 
   /**
    * Opens the chat window
    *
-   * @param {Object} contentWindow The window to open the chat window in, may
-   *                               be null.
-   * @param {String} title The title of the chat window.
-   * @param {String} url The page to load in the chat window.
+   * @param {Object} conversationWindowData The data to be obtained by the
+   *                                        window when it opens.
+   * @returns {Number} The id of the window.
    */
-  openChatWindow: function(contentWindow, title, url) {
+  openChatWindow: function(conversationWindowData) {
     // So I guess the origin is the loop server!?
     let origin = this.loopServerUri;
-    url = url.spec || url;
+    let windowId = gLastWindowId++;
+    // Store the id as a string, as that's what we use elsewhere.
+    windowId = windowId.toString();
+
+    gConversationWindowData.set(windowId, conversationWindowData);
+
+    let url = "about:loopconversation#" + windowId;
 
     let callback = chatbox => {
       // We need to use DOMContentLoaded as otherwise the injection will happen
       // in about:blank and then get lost.
       // Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
       // involves event loop spins, which means it might be too late.
       // Have we already done it?
       if (chatbox.contentWindow.navigator.mozLoop) {
@@ -744,17 +751,18 @@ let MozLoopServiceInternal = {
           }
         };
 
         let pc_static = new window.mozRTCPeerConnectionStatic();
         pc_static.registerPeerConnectionLifecycleCallback(onPCLifecycleChange);
       }.bind(this), true);
     };
 
-    Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
+    Chat.open(null, origin, "", url, undefined, undefined, callback);
+    return windowId;
   },
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
    */
   promiseFxAOAuthParameters: function() {
@@ -994,23 +1002,22 @@ this.MozLoopService = {
       deferredInitialization.reject("error logging in using cached auth token");
     });
     yield completedPromise;
   }),
 
   /**
    * Opens the chat window
    *
-   * @param {Object} contentWindow The window to open the chat window in, may
-   *                               be null.
-   * @param {String} title The title of the chat window.
-   * @param {String} url The page to load in the chat window.
+   * @param {Object} conversationWindowData The data to be obtained by the
+   *                                        window when it opens.
+   * @returns {Number} The id of the window.
    */
-  openChatWindow: function(contentWindow, title, url) {
-    MozLoopServiceInternal.openChatWindow(contentWindow, title, url);
+  openChatWindow: function(conversationWindowData) {
+    return MozLoopServiceInternal.openChatWindow(conversationWindowData);
   },
 
   /**
    * If we're operating the service in "soft start" mode, and this browser
    * isn't already activated, check whether it's time for it to become active.
    * If so, activate the loop service.
    *
    * @param {Function} doneCb   [optional] Callback that is called when the
@@ -1410,9 +1417,29 @@ this.MozLoopService = {
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
   hawkRequest: function(sessionType, path, method, payloadObj) {
     return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj).catch(
       error => {MozLoopServiceInternal._hawkRequestError(error);});
   },
+
+  /**
+   * Returns the window data for a specific conversation window id.
+   *
+   * This data will be relevant to the type of window, e.g. rooms or calls.
+   * See LoopRooms or LoopCalls for more information.
+   *
+   * @param {String} conversationWindowId
+   * @returns {Object} The window data or null if error.
+   */
+  getConversationWindowData: function(conversationWindowId) {
+    if (gConversationWindowData.has(conversationWindowId)) {
+      var conversationData = gConversationWindowData.get(conversationWindowId);
+      gConversationWindowData.delete(conversationWindowId);
+      return conversationData;
+    }
+
+    log.error("Window data was already fetched before. Possible race condition!");
+    return null;
+  }
 };
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -387,22 +387,22 @@ loop.contacts = (function(_, mozL10n) {
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
           if (!contact.blocked) {
-            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
           }
           break;
         case "audio-call":
           if (!contact.blocked) {
-            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
           }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -387,22 +387,22 @@ loop.contacts = (function(_, mozL10n) {
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
         case "video-call":
           if (!contact.blocked) {
-            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
           }
           break;
         case "audio-call":
           if (!contact.blocked) {
-            navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+            navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
           }
           break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -213,17 +213,19 @@ loop.conversation = (function(mozL10n) {
    *
    * At the moment, it does more than that, these parts need refactoring out.
    */
   var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
-      sdk: React.PropTypes.object.isRequired
+      sdk: React.PropTypes.object.isRequired,
+      conversationAppStore: React.PropTypes.instanceOf(
+        loop.store.ConversationAppStore).isRequired
     },
 
     getInitialState: function() {
       return {
         callFailed: false, // XXX this should be removed when bug 1047410 lands.
         callStatus: "start"
       };
     },
@@ -347,39 +349,36 @@ loop.conversation = (function(mozL10n) {
     },
 
     /**
      * Incoming call route.
      */
     setupIncomingCall: function() {
       navigator.mozLoop.startAlerting();
 
-      var callData = navigator.mozLoop.getCallData(this.props.conversation.get("windowId"));
-      if (!callData) {
-        // XXX Not the ideal response, but bug 1047410 will be replacing
-        // this by better "call failed" UI.
-        console.error("Failed to get the call data");
-        return;
-      }
+      // XXX This is a hack until we rework for the flux model in bug 1088672.
+      var callData = this.props.conversationAppStore.getStoreState().windowData;
+
       this.props.conversation.setIncomingSessionData(callData);
       this._setupWebSocket();
     },
 
     /**
      * Starts the actual conversation
      */
     accepted: function() {
       this.setState({callStatus: "connected"});
     },
 
     /**
      * Moves the call to the end state
      */
     endCall: function() {
-      navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
       this.setState({callStatus: "end"});
     },
 
     /**
      * Used to set up the web socket connection and navigate to the
      * call view if appropriate.
      */
     _setupWebSocket: function() {
@@ -470,17 +469,18 @@ loop.conversation = (function(mozL10n) {
       this.props.conversation.accepted();
     },
 
     /**
      * Declines a call and handles closing of the window.
      */
     _declineCall: function() {
       this._websocket.decline();
-      navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
       this._websocket.close();
       // Having a timeout here lets the logging for the websocket complete and be
       // displayed on the console if both are on.
       setTimeout(this.closeWindow, 0);
     },
 
     /**
      * Declines an incoming call.
@@ -562,34 +562,35 @@ loop.conversation = (function(mozL10n) {
     },
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (IncomingConversationView({
             client: this.props.client, 
             conversation: this.props.conversation, 
-            sdk: this.props.sdk}
+            sdk: this.props.sdk, 
+            conversationAppStore: this.props.conversationAppStore}
           ));
         }
         case "outgoing": {
           return (OutgoingConversationView({
             store: this.props.conversationStore, 
             dispatcher: this.props.dispatcher}
           ));
         }
         case "room": {
           return (EmptyRoomView({
             mozLoop: navigator.mozLoop, 
             localRoomStore: this.props.localRoomStore}
           ));
         }
         case "failed": {
           return (GenericFailureView({
-            cancelCall: this.closeWindow.bind(this)}
+            cancelCall: this.closeWindow}
           ));
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
         }
       }
@@ -654,17 +655,17 @@ loop.conversation = (function(mozL10n) {
     if (hash) {
       windowId = hash[1];
     }
 
     conversation.set({windowId: windowId});
 
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
-      navigator.mozLoop.releaseCallData(windowId);
+      navigator.mozLoop.calls.clearCallInProgress(windowId);
     });
 
     React.renderComponent(AppControllerView({
       conversationAppStore: conversationAppStore, 
       localRoomStore: localRoomStore, 
       conversationStore: conversationStore, 
       client: client, 
       conversation: conversation, 
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -213,17 +213,19 @@ loop.conversation = (function(mozL10n) {
    *
    * At the moment, it does more than that, these parts need refactoring out.
    */
   var IncomingConversationView = React.createClass({
     propTypes: {
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
-      sdk: React.PropTypes.object.isRequired
+      sdk: React.PropTypes.object.isRequired,
+      conversationAppStore: React.PropTypes.instanceOf(
+        loop.store.ConversationAppStore).isRequired
     },
 
     getInitialState: function() {
       return {
         callFailed: false, // XXX this should be removed when bug 1047410 lands.
         callStatus: "start"
       };
     },
@@ -347,39 +349,36 @@ loop.conversation = (function(mozL10n) {
     },
 
     /**
      * Incoming call route.
      */
     setupIncomingCall: function() {
       navigator.mozLoop.startAlerting();
 
-      var callData = navigator.mozLoop.getCallData(this.props.conversation.get("windowId"));
-      if (!callData) {
-        // XXX Not the ideal response, but bug 1047410 will be replacing
-        // this by better "call failed" UI.
-        console.error("Failed to get the call data");
-        return;
-      }
+      // XXX This is a hack until we rework for the flux model in bug 1088672.
+      var callData = this.props.conversationAppStore.getStoreState().windowData;
+
       this.props.conversation.setIncomingSessionData(callData);
       this._setupWebSocket();
     },
 
     /**
      * Starts the actual conversation
      */
     accepted: function() {
       this.setState({callStatus: "connected"});
     },
 
     /**
      * Moves the call to the end state
      */
     endCall: function() {
-      navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
       this.setState({callStatus: "end"});
     },
 
     /**
      * Used to set up the web socket connection and navigate to the
      * call view if appropriate.
      */
     _setupWebSocket: function() {
@@ -470,17 +469,18 @@ loop.conversation = (function(mozL10n) {
       this.props.conversation.accepted();
     },
 
     /**
      * Declines a call and handles closing of the window.
      */
     _declineCall: function() {
       this._websocket.decline();
-      navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
       this._websocket.close();
       // Having a timeout here lets the logging for the websocket complete and be
       // displayed on the console if both are on.
       setTimeout(this.closeWindow, 0);
     },
 
     /**
      * Declines an incoming call.
@@ -563,33 +563,34 @@ loop.conversation = (function(mozL10n) {
 
     render: function() {
       switch(this.state.windowType) {
         case "incoming": {
           return (<IncomingConversationView
             client={this.props.client}
             conversation={this.props.conversation}
             sdk={this.props.sdk}
+            conversationAppStore={this.props.conversationAppStore}
           />);
         }
         case "outgoing": {
           return (<OutgoingConversationView
             store={this.props.conversationStore}
             dispatcher={this.props.dispatcher}
           />);
         }
         case "room": {
           return (<EmptyRoomView
             mozLoop={navigator.mozLoop}
             localRoomStore={this.props.localRoomStore}
           />);
         }
         case "failed": {
           return (<GenericFailureView
-            cancelCall={this.closeWindow.bind(this)}
+            cancelCall={this.closeWindow}
           />);
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
         }
       }
@@ -654,17 +655,17 @@ loop.conversation = (function(mozL10n) {
     if (hash) {
       windowId = hash[1];
     }
 
     conversation.set({windowId: windowId});
 
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
-      navigator.mozLoop.releaseCallData(windowId);
+      navigator.mozLoop.calls.clearCallInProgress(windowId);
     });
 
     React.renderComponent(<AppControllerView
       conversationAppStore={conversationAppStore}
       localRoomStore={localRoomStore}
       conversationStore={conversationStore}
       client={client}
       conversation={conversation}
--- a/browser/components/loop/content/js/conversationAppStore.js
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -61,28 +61,32 @@ loop.store.ConversationAppStore = (funct
      * @param {sharedActions.GetWindowData} actionData The action data
      */
     getWindowData: function(actionData) {
       var windowData;
       // XXX Remove me in bug 1074678
       if (this._mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
         windowData = {type: "room", localRoomId: "42"};
       } else {
-        windowData = this._mozLoop.getCallData(actionData.windowId);
+        windowData = this._mozLoop.getConversationWindowData(actionData.windowId);
       }
 
       if (!windowData) {
         console.error("Failed to get the window data");
         this.setStoreState({windowType: "failed"});
         return;
       }
 
-      this.setStoreState({windowType: windowData.type});
+      // XXX windowData is a hack for the IncomingConversationView until
+      // we rework it for the flux model in bug 1088672.
+      this.setStoreState({
+        windowType: windowData.type,
+        windowData: windowData
+      });
 
-      this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData({
-        windowData: windowData
-      }));
+      this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData(_.extend({
+        windowId: actionData.windowId}, windowData)));
     }
   }, Backbone.Events);
 
   return ConversationAppStore;
 
 })();
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -37,17 +37,23 @@ loop.shared.actions = (function() {
       windowId: String
     }),
 
     /**
      * Used to pass round the window data so that stores can
      * record the appropriate data.
      */
     SetupWindowData: Action.define("setupWindowData", {
-      windowData: Object
+      windowId: String,
+      type: String
+
+      // Optional Items. There are other optional items typically sent
+      // around with this action. They are for the setup of calls and rooms and
+      // depend on the type. See LoopCalls and LoopRooms for the details of this
+      // data.
     }),
 
     /**
      * Fetch a new call url from the server, intended to be sent over email when
      * a contact can't be reached.
      */
     FetchEmailLink: Action.define("fetchEmailLink", {
     }),
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -184,31 +184,30 @@ loop.store.ConversationStore = (function
         default: {
           console.error("Unexpected websocket state passed to connectionProgress:",
             actionData.wsState);
         }
       }
     },
 
     setupWindowData: function(actionData) {
-      var windowData = actionData.windowData;
-      var windowType = windowData.type;
+      var windowType = actionData.type;
       if (windowType !== "outgoing" &&
           windowType !== "incoming") {
         // Not for this store, don't do anything.
         return;
       }
 
       this.set({
-        contact: windowData.contact,
+        contact: actionData.contact,
         outgoing: windowType === "outgoing",
-        windowId: windowData.windowId,
-        callType: windowData.callType,
+        windowId: actionData.windowId,
+        callType: actionData.callType,
         callState: CALL_STATES.GATHER,
-        videoMuted: windowData.callType === CALL_TYPES.AUDIO_ONLY
+        videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
       });
 
       if (this.get("outgoing")) {
         this._setupOutgoingCall();
       } // XXX Else, other types aren't supported yet.
     },
 
     /**
@@ -312,16 +311,18 @@ loop.store.ConversationStore = (function
     /**
      * Obtains the outgoing call data from the server and handles the
      * result.
      */
     _setupOutgoingCall: function() {
       var contactAddresses = [];
       var contact = this.get("contact");
 
+      navigator.mozLoop.calls.setCallInProgress(this.get("windowId"));
+
       function appendContactValues(property, strip) {
         if (contact.hasOwnProperty(property)) {
           contact[property].forEach(function(item) {
             if (strip) {
               contactAddresses.push(item.value
                 .replace(/^(\+)?(.*)$/g, function(m, prefix, number) {
                   return (prefix || "") + number.replace(/[\D]+/g, "");
                 }));
@@ -391,17 +392,17 @@ loop.store.ConversationStore = (function
       if (this._websocket) {
         this.stopListening(this._websocket);
 
         // Now close the websocket.
         this._websocket.close();
         delete this._websocket;
       }
 
-      navigator.mozLoop.releaseCallData(this.get("windowId"));
+      navigator.mozLoop.calls.clearCallInProgress(this.get("windowId"));
     },
 
     /**
      * Used to handle any progressed received from the websocket. This will
      * dispatch new actions so that the data can be handled appropriately.
      */
     _handleWebSocketProgress: function(progressData) {
       var action;
--- a/browser/components/loop/content/shared/js/localRoomStore.js
+++ b/browser/components/loop/content/shared/js/localRoomStore.js
@@ -95,26 +95,26 @@ loop.store.LocalRoomStore = (function() 
      * roomName as specified to the createRoom method.
      *
      * When the room name gets set, that will trigger the view to display
      * that name.
      *
      * @param {sharedActions.SetupWindowData} actionData
      */
     setupWindowData: function(actionData) {
-      if (actionData.windowData.type !== "room") {
+      if (actionData.type !== "room") {
         // Nothing for us to do here, leave it to other stores.
         return;
       }
 
-      this._fetchRoomData(actionData.windowData.localRoomId,
+      this._fetchRoomData(actionData.localRoomId,
         function(error, roomData) {
           this.setStoreState({
             error: error,
-            localRoomId: actionData.windowData.localRoomId,
+            localRoomId: actionData.localRoomId,
             serverData: roomData
           });
         }.bind(this));
     }
 
   }, Backbone.Events);
 
   return LocalRoomStore;
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -27,58 +27,61 @@ describe("loop.store.ConversationAppStor
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
         new loop.store.ConversationAppStore({dispatcher: dispatcher});
       }).to.Throw(/mozLoop/);
     });
   });
 
   describe("#getWindowData", function() {
-    var fakeCallData, fakeGetWindowData, fakeMozLoop, store;
+    var fakeWindowData, fakeGetWindowData, fakeMozLoop, store;
 
     beforeEach(function() {
-      fakeCallData = {
+      fakeWindowData = {
         type: "incoming",
         callId: "123456"
       };
 
       fakeGetWindowData = {
         windowId: "42"
       };
 
       fakeMozLoop = {
         // XXX Remove me in bug 1074678
         getLoopBoolPref: function() { return false; },
-        getCallData: function(windowId) {
+        getConversationWindowData: function(windowId) {
           if (windowId === "42") {
-            return fakeCallData;
+            return fakeWindowData;
           }
           return null;
         }
       };
 
       store = new loop.store.ConversationAppStore({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop
       });
     });
 
     it("should fetch the window type from the mozLoop API", function() {
       dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
 
-      expect(store.getStoreState()).eql({windowType: "incoming"});
+      expect(store.getStoreState()).eql({
+        windowType: "incoming",
+        windowData: fakeWindowData
+      });
     });
 
     it("should dispatch a SetupWindowData action with the data from the mozLoop API",
       function() {
         sandbox.stub(dispatcher, "dispatch");
 
         store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.SetupWindowData({
-            windowData: fakeCallData
-          }));
+          new sharedActions.SetupWindowData(_.extend({
+            windowId: fakeGetWindowData.windowId
+          }, fakeWindowData)));
       });
   });
 
 });
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -36,18 +36,19 @@ describe("loop.conversation", function()
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
       setLoopCharPref: sinon.stub(),
       getLoopCharPref: sinon.stub().returns("http://fakeurl"),
       getLoopBoolPref: sinon.stub(),
-      getCallData: sinon.stub(),
-      releaseCallData: sinon.stub(),
+      calls: {
+        clearCallInProgress: sinon.stub()
+      },
       startAlerting: sinon.stub(),
       stopAlerting: sinon.stub(),
       ensureRegistered: sinon.stub(),
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
           platform: "test"
@@ -180,16 +181,23 @@ describe("loop.conversation", function()
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversationViews.OutgoingConversationView);
     });
 
     it("should display the IncomingConversationView for incoming calls", function() {
+      sandbox.stub(conversation, "setIncomingSessionData");
+      sandbox.stub(loop, "CallConnectionWebSocket").returns({
+        promiseConnect: function() {
+          return new Promise(function() {});
+        },
+        on: sandbox.spy()
+      });
       conversationAppStore.setStoreState({windowType: "incoming"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversation.IncomingConversationView);
     });
 
@@ -208,256 +216,254 @@ describe("loop.conversation", function()
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversation.GenericFailureView);
     });
   });
 
   describe("IncomingConversationView", function() {
-    var conversation, client, icView, oldTitle;
+    var conversationAppStore, conversation, client, icView, oldTitle;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.conversation.IncomingConversationView({
           client: client,
           conversation: conversation,
-          sdk: {}
+          sdk: {},
+          conversationAppStore: conversationAppStore
         }));
     }
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {}
       });
       conversation.set({windowId: 42});
+      var dispatcher = new loop.Dispatcher();
+      conversationAppStore = new loop.store.ConversationAppStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
       sandbox.stub(conversation, "setOutgoingSessionData");
     });
 
     afterEach(function() {
       icView = undefined;
       document.title = oldTitle;
     });
 
     describe("start", function() {
       it("should set the title to incoming_call_title2", function() {
-        navigator.mozLoop.getCallData = function() {
-          return {
+        conversationAppStore.setStoreState({
+          windowData: {
             progressURL:    "fake",
             websocketToken: "fake",
             callId: 42
-          };
-        };
+          }
+        });
 
         icView = mountTestComponent();
 
         expect(document.title).eql("incoming_call_title2");
       });
     });
 
     describe("componentDidMount", function() {
-      var fakeSessionData;
+      var fakeSessionData, promise, resolveWebSocketConnect;
+      var rejectWebSocketConnect;
 
       beforeEach(function() {
         fakeSessionData  = {
           sessionId:      "sessionId",
           sessionToken:   "sessionToken",
           apiKey:         "apiKey",
           callType:       "callType",
           callId:         "Hello",
           progressURL:    "http://progress.example.com",
           websocketToken: "7b"
         };
 
-        navigator.mozLoop.getCallData.returns(fakeSessionData);
+        conversationAppStore.setStoreState({
+          windowData: fakeSessionData
+        });
+
         stubComponent(loop.conversation, "IncomingCallView");
         stubComponent(sharedView, "ConversationView");
       });
 
       it("should start alerting", function() {
         icView = mountTestComponent();
 
         sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
       });
 
-      it("should call getCallData on navigator.mozLoop", function() {
-        icView = mountTestComponent();
-
-        sinon.assert.calledOnce(navigator.mozLoop.getCallData);
-        sinon.assert.calledWith(navigator.mozLoop.getCallData, 42);
-      });
-
-      describe("getCallData successful", function() {
-        var promise, resolveWebSocketConnect,
-            rejectWebSocketConnect;
-
-        describe("Session Data setup", function() {
-          beforeEach(function() {
-            sandbox.stub(loop, "CallConnectionWebSocket").returns({
-              promiseConnect: function () {
-                promise = new Promise(function(resolve, reject) {
-                  resolveWebSocketConnect = resolve;
-                  rejectWebSocketConnect = reject;
-                });
-                return promise;
-              },
-              on: sinon.stub()
-            });
-          });
-
-          it("should store the session data", function() {
-            sandbox.stub(conversation, "setIncomingSessionData");
-
-            icView = mountTestComponent();
-
-            sinon.assert.calledOnce(conversation.setIncomingSessionData);
-            sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
-                                           fakeSessionData);
-          });
-
-          it("should setup the websocket connection", function() {
-            icView = mountTestComponent();
-
-            sinon.assert.calledOnce(loop.CallConnectionWebSocket);
-            sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
-              callId: "Hello",
-              url: "http://progress.example.com",
-              websocketToken: "7b"
-            });
+      describe("Session Data setup", function() {
+        beforeEach(function() {
+          sandbox.stub(loop, "CallConnectionWebSocket").returns({
+            promiseConnect: function () {
+              promise = new Promise(function(resolve, reject) {
+                resolveWebSocketConnect = resolve;
+                rejectWebSocketConnect = reject;
+              });
+              return promise;
+            },
+            on: sinon.stub()
           });
         });
 
-        describe("WebSocket Handling", function() {
-          beforeEach(function() {
-            promise = new Promise(function(resolve, reject) {
-              resolveWebSocketConnect = resolve;
-              rejectWebSocketConnect = reject;
-            });
+        it("should store the session data", function() {
+          sandbox.stub(conversation, "setIncomingSessionData");
+
+          icView = mountTestComponent();
+
+          sinon.assert.calledOnce(conversation.setIncomingSessionData);
+          sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
+                                         fakeSessionData);
+        });
+
+        it("should setup the websocket connection", function() {
+          icView = mountTestComponent();
 
-            sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
+          sinon.assert.calledOnce(loop.CallConnectionWebSocket);
+          sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
+            callId: "Hello",
+            url: "http://progress.example.com",
+            websocketToken: "7b"
+          });
+        });
+      });
+
+      describe("WebSocket Handling", function() {
+        beforeEach(function() {
+          promise = new Promise(function(resolve, reject) {
+            resolveWebSocketConnect = resolve;
+            rejectWebSocketConnect = reject;
           });
 
-          it("should set the state to incoming on success", function(done) {
+          sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
+        });
+
+        it("should set the state to incoming on success", function(done) {
+          icView = mountTestComponent();
+          resolveWebSocketConnect("incoming");
+
+          promise.then(function () {
+            expect(icView.state.callStatus).eql("incoming");
+            done();
+          });
+        });
+
+        it("should set the state to close on success if the progress " +
+          "state is terminated", function(done) {
             icView = mountTestComponent();
-            resolveWebSocketConnect("incoming");
+            resolveWebSocketConnect("terminated");
 
             promise.then(function () {
-              expect(icView.state.callStatus).eql("incoming");
+              expect(icView.state.callStatus).eql("close");
               done();
             });
           });
 
-          it("should set the state to close on success if the progress " +
-            "state is terminated", function(done) {
-              icView = mountTestComponent();
-              resolveWebSocketConnect("terminated");
+        // XXX implement me as part of bug 1047410
+        // see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
+        it.skip("should should switch view state to failed", function(done) {
+          icView = mountTestComponent();
+          rejectWebSocketConnect();
+
+          promise.then(function() {}, function() {
+            done();
+          });
+        });
+      });
 
-              promise.then(function () {
-                expect(icView.state.callStatus).eql("close");
+      describe("WebSocket Events", function() {
+        describe("Call cancelled or timed out before acceptance", function() {
+          beforeEach(function() {
+            // Mounting the test component automatically calls the required
+            // setup functions
+            icView = mountTestComponent();
+            promise = new Promise(function(resolve, reject) {
+              resolve();
+            });
+
+            sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
+            sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
+            sandbox.stub(window, "close");
+          });
+
+          describe("progress - terminated (previousState = alerting)", function() {
+            it("should stop alerting", function(done) {
+              promise.then(function() {
+                icView._websocket.trigger("progress", {
+                  state: "terminated",
+                  reason: "timeout"
+                }, "alerting");
+
+                sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                 done();
               });
             });
 
-          // XXX implement me as part of bug 1047410
-          // see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
-          it.skip("should should switch view state to failed", function(done) {
-            icView = mountTestComponent();
-            rejectWebSocketConnect();
+            it("should close the websocket", function(done) {
+              promise.then(function() {
+                icView._websocket.trigger("progress", {
+                  state: "terminated",
+                  reason: "closed"
+                }, "alerting");
+
+                sinon.assert.calledOnce(icView._websocket.close);
+                done();
+              });
+            });
 
-            promise.then(function() {}, function() {
-              done();
+            it("should close the window", function(done) {
+              promise.then(function() {
+                icView._websocket.trigger("progress", {
+                  state: "terminated",
+                  reason: "answered-elsewhere"
+                }, "alerting");
+
+                sandbox.clock.tick(1);
+
+                sinon.assert.calledOnce(window.close);
+                done();
+              });
             });
           });
-        });
 
-        describe("WebSocket Events", function() {
-          describe("Call cancelled or timed out before acceptance", function() {
-            beforeEach(function() {
-              // Mounting the test component automatically calls the required
-              // setup functions
-              icView = mountTestComponent();
-              promise = new Promise(function(resolve, reject) {
-                resolve();
+          describe("progress - terminated (previousState not init" +
+                   " nor alerting)",
+            function() {
+              it("should set the state to end", function(done) {
+                promise.then(function() {
+                  icView._websocket.trigger("progress", {
+                    state: "terminated",
+                    reason: "media-fail"
+                  }, "connecting");
+
+                  expect(icView.state.callStatus).eql("end");
+                  done();
+                });
               });
 
-              sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
-              sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
-              sandbox.stub(window, "close");
-            });
-
-            describe("progress - terminated (previousState = alerting)", function() {
               it("should stop alerting", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "timeout"
-                  }, "alerting");
+                    reason: "media-fail"
+                  }, "connecting");
 
                   sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                   done();
                 });
               });
-
-              it("should close the websocket", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "closed"
-                  }, "alerting");
-
-                  sinon.assert.calledOnce(icView._websocket.close);
-                  done();
-                });
-              });
-
-              it("should close the window", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "answered-elsewhere"
-                  }, "alerting");
-
-                  sandbox.clock.tick(1);
-
-                  sinon.assert.calledOnce(window.close);
-                  done();
-                });
-              });
             });
-
-            describe("progress - terminated (previousState not init" +
-                     " nor alerting)",
-              function() {
-                it("should set the state to end", function(done) {
-                  promise.then(function() {
-                    icView._websocket.trigger("progress", {
-                      state: "terminated",
-                      reason: "media-fail"
-                    }, "connecting");
-
-                    expect(icView.state.callStatus).eql("end");
-                    done();
-                  });
-                });
-
-                it("should stop alerting", function(done) {
-                  promise.then(function() {
-                    icView._websocket.trigger("progress", {
-                      state: "terminated",
-                      reason: "media-fail"
-                    }, "connecting");
-
-                    sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-                    done();
-                  });
-                });
-            });
-          });
         });
       });
 
       describe("#accept", function() {
         beforeEach(function() {
           icView = mountTestComponent();
           conversation.setIncomingSessionData({
             sessionId:      "sessionId",
@@ -521,18 +527,19 @@ describe("loop.conversation", function()
           icView.decline();
 
           sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
         });
 
         it("should release callData", function() {
           icView.decline();
 
-          sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
-          sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "8699");
+          sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+          sinon.assert.calledWithExactly(
+            navigator.mozLoop.calls.clearCallInProgress, "8699");
         });
       });
 
       describe("#blocked", function() {
         var mozLoop;
 
         beforeEach(function() {
           icView = mountTestComponent();
@@ -605,23 +612,37 @@ describe("loop.conversation", function()
         });
       });
     });
 
     describe("Events", function() {
       var fakeSessionData;
 
       beforeEach(function() {
-        icView = mountTestComponent();
 
         fakeSessionData = {
           sessionId:    "sessionId",
           sessionToken: "sessionToken",
           apiKey:       "apiKey"
         };
+
+        conversationAppStore.setStoreState({
+          windowData: fakeSessionData
+        });
+
+        sandbox.stub(conversation, "setIncomingSessionData");
+        sandbox.stub(loop, "CallConnectionWebSocket").returns({
+          promiseConnect: function() {
+            return new Promise(function() {});
+          },
+          on: sandbox.spy()
+        });
+
+        icView = mountTestComponent();
+
         conversation.set("loopToken", "fakeToken");
         navigator.mozLoop.getLoopCharPref.returns("http://fake");
         stubComponent(sharedView, "ConversationView");
       });
 
       describe("call:accepted", function() {
         it("should display the ConversationView",
           function() {
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -33,17 +33,20 @@ describe("loop.store.ConversationStore",
         type: "home",
         value: "fakeEmail",
         pref: true
       }]
     };
 
     navigator.mozLoop = {
       getLoopBoolPref: sandbox.stub(),
-      releaseCallData: sandbox.stub()
+      calls: {
+        setCallInProgress: sandbox.stub(),
+        clearCallInProgress: sandbox.stub()
+      }
     };
 
     dispatcher = new loop.Dispatcher();
     client = {
       setupOutgoingCall: sinon.stub(),
       requestCallUrl: sinon.stub()
     };
     sdkDriver = {
@@ -151,18 +154,19 @@ describe("loop.store.ConversationStore",
       expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
       expect(store.get("callStateReason")).eql("fake");
     });
 
     it("should release mozLoop callsData", function() {
       dispatcher.dispatch(
         new sharedActions.ConnectionFailure({reason: "fake"}));
 
-      sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
-      sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
+      sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+      sinon.assert.calledWithExactly(
+        navigator.mozLoop.calls.clearCallInProgress, "42");
     });
   });
 
   describe("#connectionProgress", function() {
     describe("progress: init", function() {
       it("should change the state from 'gather' to 'connecting'", function() {
         store.set({callState: CALL_STATES.GATHER});
 
@@ -222,22 +226,20 @@ describe("loop.store.ConversationStore",
   });
 
   describe("#setupWindowData", function() {
     var fakeSetupWindowData;
 
     beforeEach(function() {
       store.set({callState: CALL_STATES.INIT});
       fakeSetupWindowData = {
-        windowData: {
-          type: "outgoing",
-          contact: contact,
-          windowId: "123456",
-          callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
-        }
+        windowId: "123456",
+        type: "outgoing",
+        contact: contact,
+        callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
       };
     });
 
     it("should set the state to 'gather'", function() {
       dispatcher.dispatch(
         new sharedActions.SetupWindowData(fakeSetupWindowData));
 
       expect(store.get("callState")).eql(CALL_STATES.GATHER);
@@ -265,17 +267,17 @@ describe("loop.store.ConversationStore",
           new sharedActions.SetupWindowData(fakeSetupWindowData));
 
         sinon.assert.calledOnce(client.setupOutgoingCall);
         sinon.assert.calledWith(client.setupOutgoingCall,
           ["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
       });
 
       it("should include all email addresses in the call data", function() {
-        fakeSetupWindowData.windowData.contact = {
+        fakeSetupWindowData.contact = {
           name: [ "Mr Smith" ],
           email: [{
             type: "home",
             value: "fakeEmail",
             pref: true
           },
           {
             type: "work",
@@ -288,17 +290,17 @@ describe("loop.store.ConversationStore",
           new sharedActions.SetupWindowData(fakeSetupWindowData));
 
         sinon.assert.calledOnce(client.setupOutgoingCall);
         sinon.assert.calledWith(client.setupOutgoingCall,
           ["fakeEmail", "emailFake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
       });
 
       it("should include trim phone numbers for the call data", function() {
-        fakeSetupWindowData.windowData.contact = {
+        fakeSetupWindowData.contact = {
           name: [ "Mr Smith" ],
           tel: [{
             type: "home",
             value: "+44-5667+345 496(2335)45+ 456+",
             pref: true
           }]
         };
 
@@ -306,17 +308,17 @@ describe("loop.store.ConversationStore",
           new sharedActions.SetupWindowData(fakeSetupWindowData));
 
         sinon.assert.calledOnce(client.setupOutgoingCall);
         sinon.assert.calledWith(client.setupOutgoingCall,
           ["+445667345496233545456"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
       });
 
       it("should include all email and telephone values in the call data", function() {
-        fakeSetupWindowData.windowData.contact = {
+        fakeSetupWindowData.contact = {
           name: [ "Mr Smith" ],
           email: [{
             type: "home",
             value: "fakeEmail",
             pref: true
           }, {
             type: "work",
             value: "emailFake",
@@ -499,18 +501,19 @@ describe("loop.store.ConversationStore",
       dispatcher.dispatch(new sharedActions.HangupCall());
 
       expect(store.get("callState")).eql(CALL_STATES.FINISHED);
     });
 
     it("should release mozLoop callsData", function() {
       dispatcher.dispatch(new sharedActions.HangupCall());
 
-      sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
-      sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
+      sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+      sinon.assert.calledWithExactly(
+        navigator.mozLoop.calls.clearCallInProgress, "42");
     });
   });
 
   describe("#peerHungupCall", function() {
     var wsMediaFailSpy, wsCloseSpy;
     beforeEach(function() {
       wsMediaFailSpy = sinon.spy();
       wsCloseSpy = sinon.spy();
@@ -539,18 +542,19 @@ describe("loop.store.ConversationStore",
       dispatcher.dispatch(new sharedActions.PeerHungupCall());
 
       expect(store.get("callState")).eql(CALL_STATES.FINISHED);
     });
 
     it("should release mozLoop callsData", function() {
       dispatcher.dispatch(new sharedActions.PeerHungupCall());
 
-      sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
-      sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
+      sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+      sinon.assert.calledWithExactly(
+        navigator.mozLoop.calls.clearCallInProgress, "42");
     });
   });
 
   describe("#cancelCall", function() {
     beforeEach(function() {
       store._websocket = fakeWebsocket;
 
       store.set({callState: CALL_STATES.CONNECTING});
@@ -587,18 +591,19 @@ describe("loop.store.ConversationStore",
       dispatcher.dispatch(new sharedActions.CancelCall());
 
       expect(store.get("callState")).eql(CALL_STATES.CLOSE);
     });
 
     it("should release mozLoop callsData", function() {
       dispatcher.dispatch(new sharedActions.CancelCall());
 
-      sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
-      sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
+      sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+      sinon.assert.calledWithExactly(
+        navigator.mozLoop.calls.clearCallInProgress, "42");
     });
   });
 
   describe("#retryCall", function() {
     it("should set the state to gather", function() {
       store.set({callState: CALL_STATES.TERMINATED});
 
       dispatcher.dispatch(new sharedActions.RetryCall());
--- a/browser/components/loop/test/shared/localRoomStore_test.js
+++ b/browser/components/loop/test/shared/localRoomStore_test.js
@@ -54,54 +54,51 @@ describe("loop.store.LocalRoomStore", fu
     });
 
     it("should trigger a change event", function(done) {
       store.on("change", function() {
         done();
       });
 
       dispatcher.dispatch(new sharedActions.SetupWindowData({
-        windowData: {
-          type: "room",
-          localRoomId: fakeRoomId
-        }
+        windowId: "42",
+        type: "room",
+        localRoomId: fakeRoomId
       }));
     });
 
     it("should set localRoomId on the store from the action data",
       function(done) {
 
         store.once("change", function () {
           expect(store.getStoreState()).
             to.have.property('localRoomId', fakeRoomId);
           done();
         });
 
         dispatcher.dispatch(new sharedActions.SetupWindowData({
-          windowData: {
-            type: "room",
-            localRoomId: fakeRoomId
-          }
+          windowId: "42",
+          type: "room",
+          localRoomId: fakeRoomId
         }));
       });
 
     it("should set serverData.roomName from the getRoomData callback",
       function(done) {
 
         store.once("change", function () {
           expect(store.getStoreState()).to.have.deep.property(
             'serverData.roomName', fakeRoomName);
           done();
         });
 
         dispatcher.dispatch(new sharedActions.SetupWindowData({
-          windowData: {
-            type: "room",
-            localRoomId: fakeRoomId
-          }
+          windowId: "42",
+          type: "room",
+          localRoomId: fakeRoomId
         }));
       });
 
     it("should set error on the store when getRoomData calls back an error",
       function(done) {
 
         var fakeError = new Error("fake error");
         fakeMozLoop.rooms.getRoomData.
@@ -111,17 +108,16 @@ describe("loop.store.LocalRoomStore", fu
           fakeError); // args to call the callback with...
 
         store.once("change", function() {
           expect(this.getStoreState()).to.have.property('error', fakeError);
           done();
         });
 
         dispatcher.dispatch(new sharedActions.SetupWindowData({
-          windowData: {
-            type: "room",
-            localRoomId: fakeRoomId
-          }
+          windowId: "42",
+          type: "room",
+          localRoomId: fakeRoomId
         }));
       });
 
   });
 });
--- a/browser/components/loop/test/xpcshell/test_loopservice_busy.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -21,97 +21,105 @@ let msgHandler = function(msg) {
   }
 };
 
 add_test(function test_busy_2guest_calls() {
   actionReceived = false;
 
   MozLoopService.promiseRegisteredWithServers().then(() => {
     let opened = 0;
-    Chat.open = function() {
+    let windowId;
+    Chat.open = function(contentWindow, origin, title, url) {
       opened++;
+      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
     };
 
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
 
     waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
       do_check_true(opened === 1, "should open only one chat window");
       do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.releaseCallData(firstCallId);
+      LoopCalls.clearCallInProgress(windowId);
       run_next_test();
     }, () => {
       do_throw("should have opened a chat window for first call and rejected second call");
     });
 
   });
 });
 
 add_test(function test_busy_1fxa_1guest_calls() {
   actionReceived = false;
 
   MozLoopService.promiseRegisteredWithServers().then(() => {
     let opened = 0;
-    Chat.open = function() {
+    let windowId;
+    Chat.open = function(contentWindow, origin, title, url) {
       opened++;
+      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
     };
 
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
 
     waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
       do_check_true(opened === 1, "should open only one chat window");
       do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.releaseCallData(firstCallId);
+      LoopCalls.clearCallInProgress(windowId);
       run_next_test();
     }, () => {
       do_throw("should have opened a chat window for first call and rejected second call");
     });
 
   });
 });
 
 add_test(function test_busy_2fxa_calls() {
   actionReceived = false;
 
   MozLoopService.promiseRegisteredWithServers().then(() => {
     let opened = 0;
-    Chat.open = function() {
+    let windowId;
+    Chat.open = function(contentWindow, origin, title, url) {
       opened++;
+      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
     };
 
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
 
     waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
       do_check_true(opened === 1, "should open only one chat window");
       do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.releaseCallData(firstCallId);
+      LoopCalls.clearCallInProgress(windowId);
       run_next_test();
     }, () => {
       do_throw("should have opened a chat window for first call and rejected second call");
     });
 
   });
 });
 
 add_test(function test_busy_1guest_1fxa_calls() {
   actionReceived = false;
 
   MozLoopService.promiseRegisteredWithServers().then(() => {
     let opened = 0;
-    Chat.open = function() {
+    let windowId;
+    Chat.open = function(contentWindow, origin, title, url) {
       opened++;
+      windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
     };
 
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
 
     waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
       do_check_true(opened === 1, "should open only one chat window");
       do_check_true(actionReceived, "should respond with busy/reject to second call");
-      LoopCalls.releaseCallData(firstCallId);
+      LoopCalls.clearCallInProgress(windowId);
       run_next_test();
     }, () => {
       do_throw("should have opened a chat window for first call and rejected second call");
     });
 
   });
 });
 
--- a/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
@@ -21,37 +21,37 @@ add_task(function test_startDirectCall_o
     openedUrl = url;
   };
 
   LoopCalls.startDirectCall(contact, "audio-video");
 
   do_check_true(!!openedUrl, "should open a chat window");
 
   // Stop the busy kicking in for following tests.
-  let callId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
-  LoopCalls.releaseCallData(callId);
+  let windowId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
+  LoopCalls.clearCallInProgress(windowId);
 });
 
-add_task(function test_startDirectCall_getCallData() {
+add_task(function test_startDirectCall_getConversationWindowData() {
   let openedUrl;
   Chat.open = function(contentWindow, origin, title, url) {
     openedUrl = url;
   };
 
   LoopCalls.startDirectCall(contact, "audio-video");
 
-  let callId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
+  let windowId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
 
-  let callData = LoopCalls.getCallData(callId);
+  let callData = MozLoopService.getConversationWindowData(windowId);
 
   do_check_eq(callData.callType, "audio-video", "should have the correct call type");
   do_check_eq(callData.contact, contact, "should have the contact details");
 
   // Stop the busy kicking in for following tests.
-  LoopCalls.releaseCallData(callId);
+  LoopCalls.clearCallInProgress(windowId);
 });
 
 function run_test() {
   do_register_cleanup(function() {
     // Revert original Chat.open implementation
     Chat.open = openChatOrig;
   });