Merge m-c to b-i, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Mon, 01 Sep 2014 19:31:43 -0700
changeset 226185 08f24f225a2de87481bce535f94e499ad27d8aa9
parent 226184 277dd17e847bcdacd11ef26f02e86e6223b3557a (current diff)
parent 224462 96d70813ce16a9e05e4c0d3d5bb26a690ef21d5b (diff)
child 226186 bf1664e494e0550b06a4411d08d51000d62bd531
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b-i, a=merge
--- a/accessible/base/TreeWalker.cpp
+++ b/accessible/base/TreeWalker.cpp
@@ -7,98 +7,135 @@
 
 #include "Accessible.h"
 #include "nsAccessibilityService.h"
 #include "DocAccessible.h"
 
 #include "mozilla/dom/ChildIterator.h"
 #include "mozilla/dom/Element.h"
 
-using namespace mozilla;
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
+// WalkState
+////////////////////////////////////////////////////////////////////////////////
+
+namespace mozilla {
+namespace a11y {
+
+struct WalkState
+{
+  WalkState(nsIContent *aContent, uint32_t aFilter) :
+    content(aContent), prevState(nullptr), iter(aContent, aFilter) {}
+
+  nsCOMPtr<nsIContent> content;
+  WalkState *prevState;
+  dom::AllChildrenIterator iter;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+////////////////////////////////////////////////////////////////////////////////
 // TreeWalker
 ////////////////////////////////////////////////////////////////////////////////
 
 TreeWalker::
   TreeWalker(Accessible* aContext, nsIContent* aContent, uint32_t aFlags) :
-  mDoc(aContext->Document()), mContext(aContext), mAnchorNode(aContent),
-  mFlags(aFlags)
+  mDoc(aContext->Document()), mContext(aContext),
+  mFlags(aFlags), mState(nullptr)
 {
   NS_ASSERTION(aContent, "No node for the accessible tree walker!");
 
   mChildFilter = mContext->CanHaveAnonChildren() ?
     nsIContent::eAllChildren : nsIContent::eAllButXBL;
   mChildFilter |= nsIContent::eSkipPlaceholderContent;
 
   if (aContent)
-    PushState(aContent);
+    mState = new WalkState(aContent, mChildFilter);
 
   MOZ_COUNT_CTOR(TreeWalker);
 }
 
 TreeWalker::~TreeWalker()
 {
+  // Clear state stack from memory
+  while (mState)
+    PopState();
+
   MOZ_COUNT_DTOR(TreeWalker);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // TreeWalker: private
 
 Accessible*
-TreeWalker::NextChild()
+TreeWalker::NextChildInternal(bool aNoWalkUp)
 {
-  if (mStateStack.IsEmpty())
+  if (!mState || !mState->content)
     return nullptr;
 
-  dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1];
-  while (top) {
-    while (nsIContent* childNode = top->GetNextChild()) {
-      bool isSubtreeHidden = false;
-      Accessible* accessible = mFlags & eWalkCache ?
-        mDoc->GetAccessible(childNode) :
-        GetAccService()->GetOrCreateAccessible(childNode, mContext,
-                                               &isSubtreeHidden);
+  while (nsIContent* childNode = mState->iter.GetNextChild()) {
+    bool isSubtreeHidden = false;
+    Accessible* accessible = mFlags & eWalkCache ?
+      mDoc->GetAccessible(childNode) :
+      GetAccService()->GetOrCreateAccessible(childNode, mContext,
+                                             &isSubtreeHidden);
 
+    if (accessible)
+      return accessible;
+
+    // Walk down into subtree to find accessibles.
+    if (!isSubtreeHidden && childNode->IsElement()) {
+      PushState(childNode);
+      accessible = NextChildInternal(true);
       if (accessible)
         return accessible;
+    }
+  }
 
-      // Walk down into subtree to find accessibles.
-      if (!isSubtreeHidden && childNode->IsElement())
-        top = PushState(childNode);
-    }
+  // No more children, get back to the parent.
+  nsIContent* anchorNode = mState->content;
+  PopState();
+  if (aNoWalkUp)
+    return nullptr;
 
-    top = PopState();
-  }
+  if (mState)
+    return NextChildInternal(false);
 
   // If we traversed the whole subtree of the anchor node. Move to next node
   // relative anchor node within the context subtree if possible.
   if (mFlags != eWalkContextTree)
     return nullptr;
 
-  nsINode* contextNode = mContext->GetNode();
-  while (mAnchorNode != contextNode) {
-    nsINode* parentNode = mAnchorNode->GetFlattenedTreeParent();
+  while (anchorNode != mContext->GetNode()) {
+    nsINode* parentNode = anchorNode->GetFlattenedTreeParent();
     if (!parentNode || !parentNode->IsElement())
       return nullptr;
 
-    nsIContent* parent = parentNode->AsElement();
-    top = mStateStack.AppendElement(dom::AllChildrenIterator(parent,
-                                                             mChildFilter));
-    while (nsIContent* childNode = top->GetNextChild()) {
-      if (childNode == mAnchorNode) {
-        mAnchorNode = parent;
-        return NextChild();
-      }
+    PushState(parentNode->AsElement());
+    while (nsIContent* childNode = mState->iter.GetNextChild()) {
+      if (childNode == anchorNode)
+        return NextChildInternal(false);
     }
+    PopState();
+
+    anchorNode = parentNode->AsElement();
   }
 
   return nullptr;
 }
 
-dom::AllChildrenIterator*
+void
 TreeWalker::PopState()
 {
-  size_t length = mStateStack.Length();
-  mStateStack.RemoveElementAt(length - 1);
-  return mStateStack.IsEmpty() ? nullptr : &mStateStack[mStateStack.Length() - 1];
+  WalkState* prevToLastState = mState->prevState;
+  delete mState;
+  mState = prevToLastState;
 }
+
+void
+TreeWalker::PushState(nsIContent* aContent)
+{
+  WalkState* nextToLastState = new WalkState(aContent, mChildFilter);
+  nextToLastState->prevState = mState;
+  mState = nextToLastState;
+}
--- a/accessible/base/TreeWalker.h
+++ b/accessible/base/TreeWalker.h
@@ -3,27 +3,27 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_a11y_TreeWalker_h_
 #define mozilla_a11y_TreeWalker_h_
 
 #include "mozilla/Attributes.h"
 #include <stdint.h>
-#include "mozilla/dom/ChildIterator.h"
-#include "nsCOMPtr.h"
 
 class nsIContent;
 
 namespace mozilla {
 namespace a11y {
 
 class Accessible;
 class DocAccessible;
 
+struct WalkState;
+
 /**
  * This class is used to walk the DOM tree to create accessible tree.
  */
 class TreeWalker MOZ_FINAL
 {
 public:
   enum {
     // used to walk the existing tree of the given node
@@ -45,44 +45,51 @@ public:
 
   /**
    * Return the next child accessible.
    *
    * @note Returned accessible is bound to the document, if the accessible is
    *       rejected during tree creation then the caller should be unbind it
    *       from the document.
    */
-  Accessible* NextChild();
+  Accessible* NextChild()
+  {
+    return NextChildInternal(false);
+  }
 
 private:
   TreeWalker();
   TreeWalker(const TreeWalker&);
   TreeWalker& operator =(const TreeWalker&);
 
   /**
+   * Return the next child accessible.
+   *
+   * @param  aNoWalkUp  [in] specifies the walk direction, true means we
+   *                     shouldn't go up through the tree if we failed find
+   *                     accessible children.
+   */
+  Accessible* NextChildInternal(bool aNoWalkUp);
+
+  /**
    * Create new state for the given node and push it on top of stack.
    *
    * @note State stack is used to navigate up/down the DOM subtree during
    *        accessible children search.
    */
-  dom::AllChildrenIterator* PushState(nsIContent* aContent)
-  {
-    return mStateStack.AppendElement(dom::AllChildrenIterator(aContent,
-                                                              mChildFilter));
-  }
+  void PushState(nsIContent* aNode);
 
   /**
    * Pop state from stack.
    */
-  dom::AllChildrenIterator* PopState();
+  void PopState();
 
   DocAccessible* mDoc;
   Accessible* mContext;
-  nsIContent* mAnchorNode;
-  nsAutoTArray<dom::AllChildrenIterator, 20> mStateStack;
   int32_t mChildFilter;
   uint32_t mFlags;
+  WalkState* mState;
 };
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif // mozilla_a11y_TreeWalker_h_
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1574,18 +1574,18 @@ pref("pdfjs.previousHandler.alwaysAskBef
 pref("shumway.disabled", true);
 #endif
 
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
-// Enable by default on nightly and aurora.
-#ifndef RELEASE_BUILD
+// Enable by default development builds up until early beta
+#ifdef EARLY_BETA_OR_EARLIER
 pref("loop.enabled", true);
 #else
 pref("loop.enabled", false);
 #endif
 
 pref("loop.server", "https://loop.services.mozilla.com");
 pref("loop.seenToS", "unseen");
 pref("loop.legal.ToS_url", "https://accounts.firefox.com/legal/terms");
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -8,16 +8,21 @@ let LoopUI;
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 
 
 (function() {
 
   LoopUI = {
+    get toolbarButton() {
+      delete this.toolbarButton;
+      return this.toolbarButton = CustomizableUI.getWidget("loop-call-button").forWindow(window);
+    },
+
     /**
      * Opens the panel for Loop and sizes it appropriately.
      *
      * @param {event} event The event opening the panel, used to anchor
      *                      the panel to the button which triggers it.
      */
     openCallPanel: function(event) {
       let callback = iframe => {
@@ -30,18 +35,45 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       PanelFrame.showPopup(window, event.target, "loop", null,
                            "about:looppanel", null, callback);
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
-    initialize: function() {
+    init: function() {
       if (!Services.prefs.getBoolPref("loop.enabled")) {
-        CustomizableUI.getWidget("loop-call-button").forWindow(window).node.hidden = true;
+        this.toolbarButton.node.hidden = true;
         return;
       }
 
+      // Add observer notifications before the service is initialized
+      Services.obs.addObserver(this, "loop-status-changed", false);
+
+
       MozLoopService.initialize();
+      this.updateToolbarState();
+    },
+
+    uninit: function() {
+      Services.obs.removeObserver(this, "loop-status-changed");
+    },
+
+    // Implements nsIObserver
+    observe: function(subject, topic, data) {
+      if (topic != "loop-status-changed") {
+        return;
+      }
+      this.updateToolbarState();
+    },
+
+    updateToolbarState: function() {
+      let state = "";
+      if (MozLoopService.errors.size) {
+        state = "error";
+      } else if (MozLoopService.doNotDisturb) {
+        state = "disabled";
+      }
+      this.toolbarButton.node.setAttribute("state", state);
     },
   };
 })();
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1208,17 +1208,17 @@ var gBrowserInit = {
     gSyncUI.init();
     gFxAccounts.init();
 #endif
 
 #ifdef MOZ_DATA_REPORTING
     gDataNotificationInfoBar.init();
 #endif
 
-    LoopUI.initialize();
+    LoopUI.init();
 
     gBrowserThumbnails.init();
 
     // Add Devtools menuitems and listeners
     gDevToolsBrowser.registerBrowserWindow(window);
 
     window.addEventListener("mousemove", MousePosTracker, false);
     window.addEventListener("dragover", MousePosTracker, false);
@@ -1379,16 +1379,17 @@ var gBrowserInit = {
       if (Win7Features)
         Win7Features.onCloseWindow();
 
       gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
       ctrlTab.uninit();
       TabView.uninit();
       SocialUI.uninit();
       gBrowserThumbnails.uninit();
+      LoopUI.uninit();
       FullZoom.destroy();
 
       Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
--- a/browser/components/loop/LoopContacts.jsm
+++ b/browser/components/loop/LoopContacts.jsm
@@ -431,17 +431,19 @@ let LoopContactsInternal = Object.freeze
         try {
           request = store.delete(guid);
         } catch (ex) {
           callback(ex);
           return;
         }
 
         request.onsuccess = event => {
-          eventEmitter.emit("remove", contact);
+          if (contact) {
+            eventEmitter.emit("remove", contact);
+          }
           callback(null, event.target.result);
         };
         request.onerror = event => callback(event.target.error);
       }, "readwrite");
     });
   },
 
   /**
@@ -669,16 +671,17 @@ let LoopContactsInternal = Object.freeze
       if (err) {
         callback(err);
         return;
       }
 
       if (!contact) {
         callback(new Error("Contact with " + kKeyPath + " '" +
                            guid + "' could not be found"));
+        return;
       }
 
       LoopStorage.getStore(kObjectStoreName, (err, store) => {
         if (err) {
           callback(err);
           return;
         }
 
@@ -718,16 +721,17 @@ let LoopContactsInternal = Object.freeze
       if (err) {
         callback(err);
         return;
       }
 
       if (!contact) {
         callback(new Error("Contact with " + kKeyPath + " '" +
                            guid + "' could not be found"));
+        return;
       }
 
       contact.blocked = true;
       this.update(contact, callback);
     });
   },
 
   /**
@@ -744,16 +748,17 @@ let LoopContactsInternal = Object.freeze
       if (err) {
         callback(err);
         return;
       }
 
       if (!contact) {
         callback(new Error("Contact with " + kKeyPath + " '" +
                            guid + "' could not be found"));
+        return;
       }
 
       contact.blocked = false;
       this.update(contact, callback);
     });
   },
 
   /**
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -56,16 +56,17 @@ let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
 let gRegisteredLoopServer = false;
 let gLocalizedStrings =  null;
 let gInitializeTimer = null;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
 let gFxAOAuthTokenData = null;
+let gErrors = 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.
  */
@@ -130,16 +131,40 @@ let MozLoopServiceInternal = {
 
   /**
    * Sets MozLoopService "do not disturb" pref value.
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
+    this.notifyStatusChanged();
+  },
+
+  notifyStatusChanged: function() {
+    Services.obs.notifyObservers(null, "loop-status-changed", null);
+  },
+
+  /**
+   * @param {String} errorType a key to identify the type of error. Only one
+   *                           error of a type will be saved at a time.
+   * @param {Object} error     an object describing the error in the format from Hawk errors
+   */
+  setError: function(errorType, error) {
+    gErrors.set(errorType, error);
+    this.notifyStatusChanged();
+  },
+
+  clearError: function(errorType) {
+    gErrors.delete(errorType);
+    this.notifyStatusChanged();
+  },
+
+  get errors() {
+    return gErrors;
   },
 
   /**
    * Starts registration of Loop with the push server, and then will register
    * with the Loop server. It will return early if already registered.
    *
    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
    *                                 to allow mocking of the MozLoopPushHandler.
@@ -252,16 +277,17 @@ let MozLoopServiceInternal = {
     this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl})
       .then((response) => {
         // If this failed we got an invalid token. storeSessionToken rejects
         // the gRegisteredDeferred promise for us, so here we just need to
         // early return.
         if (!this.storeSessionToken(response.headers))
           return;
 
+        this.clearError("registration");
         gRegisteredDeferred.resolve();
         // No need to clear the promise here, everything was good, so we don't need
         // to re-register.
       }, (error) => {
         // There's other errors than invalid auth token, but we should only do the reset
         // as a last resort.
         if (error.code === 401 && error.errno === INVALID_AUTH_TOKEN) {
           if (this.urlExpiryTimeIsInFuture()) {
@@ -273,16 +299,17 @@ let MozLoopServiceInternal = {
           // Authorization failed, invalid token, we need to try again with a new token.
           Services.prefs.clearUserPref("loop.hawk-session-token");
           this.registerWithLoopServer(pushUrl, true);
           return;
         }
 
         // XXX Bubble the precise details up to the UI somehow (bug 1013248).
         Cu.reportError("Failed to register with the loop server. error: " + error);
+        this.setError("registration", error);
         gRegisteredDeferred.reject(error.errno);
         gRegisteredDeferred = null;
       }
     );
   },
 
   /**
    * Callback from MozLoopPushHandler - A push notification has been received from
@@ -296,17 +323,17 @@ let MozLoopServiceInternal = {
     }
 
     // We set this here as it is assumed that once the user receives an incoming
     // call, they'll have had enough time to see the terms of service. See
     // bug 1046039 for background.
     Services.prefs.setCharPref("loop.seenToS", "seen");
 
     this.openChatWindow(null,
-                        this.localizedStrings["incoming_call_title"].textContent,
+                        this.localizedStrings["incoming_call_title2"].textContent,
                         "about:loopconversation#incoming/" + version);
   },
 
   /**
    * A getter to obtain and store the strings for loop. This is structured
    * for use by l10n.js.
    *
    * @returns {Object} a map of element ids with attributes to set.
@@ -690,16 +717,20 @@ this.MozLoopService = {
    * Sets MozLoopService "do not disturb" value.
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     MozLoopServiceInternal.doNotDisturb = aFlag;
   },
 
+  get errors() {
+    return MozLoopServiceInternal.errors;
+  },
+
   /**
    * Returns the current locale
    *
    * @return {String} The code of the current locale.
    */
   get locale() {
     try {
       return Services.prefs.getComplexValue("general.useragent.locale",
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -82,58 +82,58 @@ loop.conversation = (function(OT, mozL10
       var conversationPanelClass = "incoming-call";
       var dropdownMenuClassesDecline = React.addons.classSet({
         "native-dropdown-menu": true,
         "conversation-window-dropdown": true,
         "visually-hidden": !this.state.showDeclineMenu
       });
       return (
         React.DOM.div({className: conversationPanelClass}, 
-          React.DOM.h2(null, __("incoming_call")), 
+          React.DOM.h2(null, __("incoming_call_title2")), 
           React.DOM.div({className: "btn-group incoming-call-action-group"}, 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}), 
 
             React.DOM.div({className: "btn-chevron-menu-group"}, 
               React.DOM.div({className: "btn-group-chevron"}, 
                 React.DOM.div({className: "btn-group"}, 
 
                   React.DOM.button({className: btnClassDecline, 
                           onClick: this._handleDecline}, 
-                    __("incoming_call_decline_button")
+                    __("incoming_call_cancel_button")
                   ), 
                   React.DOM.div({className: "btn-chevron", 
                        onClick: this._toggleDeclineMenu}
                   )
                 ), 
 
                 React.DOM.ul({className: dropdownMenuClassesDecline}, 
                   React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, 
-                    __("incoming_call_decline_and_block_button")
+                    __("incoming_call_cancel_and_block_button")
                   )
                 )
 
               )
             ), 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}), 
 
             React.DOM.div({className: "btn-chevron-menu-group"}, 
               React.DOM.div({className: "btn-group"}, 
                 React.DOM.button({className: btnClassAccept, 
                         onClick: this._handleAccept("audio-video")}, 
                   React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
-                    __("incoming_call_answer_button")
+                    __("incoming_call_accept_button")
                   ), 
                   React.DOM.span({className: "fx-embedded-btn-icon-video"}
                   )
                 ), 
                 React.DOM.div({className: "call-audio-only", 
                      onClick: this._handleAccept("audio"), 
-                     title: __("incoming_call_answer_audio_only_tooltip")}
+                     title: __("incoming_call_accept_audio_only_tooltip")}
                 )
               )
             ), 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"})
 
           )
         )
@@ -329,17 +329,17 @@ loop.conversation = (function(OT, mozL10
       // this by better "call failed" UI.
       this._notifier.errorL10n("cannot_start_call_session_not_ready");
     },
 
     /**
      * Call has ended, display a feedback form.
      */
     feedback: function() {
-      document.title = mozL10n.get("call_has_ended");
+      document.title = mozL10n.get("conversation_has_ended");
 
       var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
         "feedback.baseUrl");
 
       var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
       var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
         product: navigator.mozLoop.getLoopCharPref("feedback.product"),
@@ -357,17 +357,17 @@ loop.conversation = (function(OT, mozL10
   /**
    * Panel initialisation.
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
-    document.title = mozL10n.get("incoming_call_title");
+    document.title = mozL10n.get("incoming_call_title2");
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
     var client = new loop.Client();
     router = new ConversationRouter({
       client: client,
       conversation: new loop.shared.models.ConversationModel(
         {},         // Model attributes
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -82,58 +82,58 @@ loop.conversation = (function(OT, mozL10
       var conversationPanelClass = "incoming-call";
       var dropdownMenuClassesDecline = React.addons.classSet({
         "native-dropdown-menu": true,
         "conversation-window-dropdown": true,
         "visually-hidden": !this.state.showDeclineMenu
       });
       return (
         <div className={conversationPanelClass}>
-          <h2>{__("incoming_call")}</h2>
+          <h2>{__("incoming_call_title2")}</h2>
           <div className="btn-group incoming-call-action-group">
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
             <div className="btn-chevron-menu-group">
               <div className="btn-group-chevron">
                 <div className="btn-group">
 
                   <button className={btnClassDecline}
                           onClick={this._handleDecline}>
-                    {__("incoming_call_decline_button")}
+                    {__("incoming_call_cancel_button")}
                   </button>
                   <div className="btn-chevron"
                        onClick={this._toggleDeclineMenu}>
                   </div>
                 </div>
 
                 <ul className={dropdownMenuClassesDecline}>
                   <li className="btn-block" onClick={this._handleDeclineBlock}>
-                    {__("incoming_call_decline_and_block_button")}
+                    {__("incoming_call_cancel_and_block_button")}
                   </li>
                 </ul>
 
               </div>
             </div>
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
             <div className="btn-chevron-menu-group">
               <div className="btn-group">
                 <button className={btnClassAccept}
                         onClick={this._handleAccept("audio-video")}>
                   <span className="fx-embedded-answer-btn-text">
-                    {__("incoming_call_answer_button")}
+                    {__("incoming_call_accept_button")}
                   </span>
                   <span className="fx-embedded-btn-icon-video">
                   </span>
                 </button>
                 <div className="call-audio-only"
                      onClick={this._handleAccept("audio")}
-                     title={__("incoming_call_answer_audio_only_tooltip")} >
+                     title={__("incoming_call_accept_audio_only_tooltip")} >
                 </div>
               </div>
             </div>
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
           </div>
         </div>
@@ -329,17 +329,17 @@ loop.conversation = (function(OT, mozL10
       // this by better "call failed" UI.
       this._notifier.errorL10n("cannot_start_call_session_not_ready");
     },
 
     /**
      * Call has ended, display a feedback form.
      */
     feedback: function() {
-      document.title = mozL10n.get("call_has_ended");
+      document.title = mozL10n.get("conversation_has_ended");
 
       var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
         "feedback.baseUrl");
 
       var appVersionInfo = navigator.mozLoop.appVersionInfo;
 
       var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
         product: navigator.mozLoop.getLoopCharPref("feedback.product"),
@@ -357,17 +357,17 @@ loop.conversation = (function(OT, mozL10
   /**
    * Panel initialisation.
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
-    document.title = mozL10n.get("incoming_call_title");
+    document.title = mozL10n.get("incoming_call_title2");
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
     var client = new loop.Client();
     router = new ConversationRouter({
       client: client,
       conversation: new loop.shared.models.ConversationModel(
         {},         // Model attributes
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -17,32 +17,55 @@ loop.panel = (function(_, mozL10n) {
 
   /**
    * Panel router.
    * @type {loop.desktopRouter.DesktopRouter}
    */
   var router;
 
   /**
-   * Availability drop down menu subview.
+   * Dropdown menu mixin.
+   * @type {Object}
    */
-  var AvailabilityDropdown = React.createClass({displayName: 'AvailabilityDropdown',
+  var DropdownMenuMixin = {
     getInitialState: function() {
-      return {
-        doNotDisturb: navigator.mozLoop.doNotDisturb,
-        showMenu: false
-      };
+      return {showMenu: false};
+    },
+
+    _onBodyClick: function() {
+      this.setState({showMenu: false});
+    },
+
+    componentDidMount: function() {
+      document.body.addEventListener("click", this._onBodyClick);
+    },
+
+    componentWillUnmount: function() {
+      document.body.removeEventListener("click", this._onBodyClick);
     },
 
     showDropdownMenu: function() {
       this.setState({showMenu: true});
     },
 
     hideDropdownMenu: function() {
       this.setState({showMenu: false});
+    }
+  };
+
+  /**
+   * Availability drop down menu subview.
+   */
+  var AvailabilityDropdown = React.createClass({displayName: 'AvailabilityDropdown',
+    mixins: [DropdownMenuMixin],
+
+    getInitialState: function() {
+      return {
+        doNotDisturb: navigator.mozLoop.doNotDisturb
+      };
     },
 
     // XXX target event can either be the li, the span or the i tag
     // this makes it easier to figure out the target by making a
     // closure with the desired status already passed in.
     changeAvailability: function(newAvailabilty) {
       return function(event) {
         // Note: side effect!
@@ -64,38 +87,38 @@ loop.panel = (function(_, mozL10n) {
       // XXX https://github.com/facebook/react/issues/310 for === htmlFor
       var cx = React.addons.classSet;
       var availabilityStatus = cx({
         'status': true,
         'status-dnd': this.state.doNotDisturb,
         'status-available': !this.state.doNotDisturb
       });
       var availabilityDropdown = cx({
-        'dnd-menu': true,
+        'dropdown-menu': true,
         'hide': !this.state.showMenu
       });
       var availabilityText = this.state.doNotDisturb ?
                               __("display_name_dnd_status") :
                               __("display_name_available_status");
 
       return (
-        React.DOM.div({className: "do-not-disturb"}, 
+        React.DOM.div({className: "dropdown"}, 
           React.DOM.p({className: "dnd-status", onClick: this.showDropdownMenu}, 
             React.DOM.span(null, availabilityText), 
             React.DOM.i({className: availabilityStatus})
           ), 
           React.DOM.ul({className: availabilityDropdown, 
               onMouseLeave: this.hideDropdownMenu}, 
             React.DOM.li({onClick: this.changeAvailability("available"), 
-                className: "dnd-menu-item dnd-make-available"}, 
+                className: "dropdown-menu-item dnd-make-available"}, 
               React.DOM.i({className: "status status-available"}), 
               React.DOM.span(null, __("display_name_available_status"))
             ), 
             React.DOM.li({onClick: this.changeAvailability("do-not-disturb"), 
-                className: "dnd-menu-item dnd-make-unavailable"}, 
+                className: "dropdown-menu-item dnd-make-unavailable"}, 
               React.DOM.i({className: "status status-dnd"}), 
               React.DOM.span(null, __("display_name_dnd_status"))
             )
           )
         )
       );
     }
   });
@@ -104,17 +127,18 @@ loop.panel = (function(_, mozL10n) {
     getInitialState: function() {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
         var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
-        var tosHTML = __("legal_text_and_links2", {
+        var tosHTML = __("legal_text_and_links3", {
+          "clientShortname": __("client_shortname_fallback"),
           "terms_of_use": React.renderComponentToStaticMarkup(
             React.DOM.a({href: terms_of_use_url, target: "_blank"}, 
               __("legal_text_tos")
             )
           ),
           "privacy_notice": React.renderComponentToStaticMarkup(
             React.DOM.a({href: privacy_notice_url, target: "_blank"}, 
               __("legal_text_privacy")
@@ -124,16 +148,103 @@ loop.panel = (function(_, mozL10n) {
         return React.DOM.p({className: "terms-service", 
                   dangerouslySetInnerHTML: {__html: tosHTML}});
       } else {
         return React.DOM.div(null);
       }
     }
   });
 
+  /**
+   * Panel settings (gear) menu entry.
+   */
+  var SettingsDropdownEntry = React.createClass({displayName: 'SettingsDropdownEntry',
+    propTypes: {
+      onClick: React.PropTypes.func.isRequired,
+      label: React.PropTypes.string.isRequired,
+      icon: React.PropTypes.string,
+      displayed: React.PropTypes.bool
+    },
+
+    getDefaultProps: function() {
+      return {displayed: true};
+    },
+
+    render: function() {
+      if (!this.props.displayed) {
+        return null;
+      }
+      return (
+        React.DOM.li({onClick: this.props.onClick, className: "dropdown-menu-item"}, 
+          this.props.icon ?
+            React.DOM.i({className: "icon icon-" + this.props.icon}) :
+            null, 
+          React.DOM.span(null, this.props.label)
+        )
+      );
+    }
+  });
+
+  /**
+   * Panel settings (gear) menu.
+   */
+  var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown',
+    mixins: [DropdownMenuMixin],
+
+    handleClickSettingsEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAccountEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAuthEntry: function() {
+      if (this._isSignedIn()) {
+        // XXX to be implemented - bug 979845
+        navigator.mozLoop.logOutFromFxA();
+      } else {
+        navigator.mozLoop.logInToFxA();
+      }
+    },
+
+    _isSignedIn: function() {
+      // XXX to be implemented - bug 979845
+      return !!navigator.mozLoop.loggedInToFxA;
+    },
+
+    render: function() {
+      var cx = React.addons.classSet;
+      return (
+        React.DOM.div({className: "settings-menu dropdown"}, 
+          React.DOM.a({className: "btn btn-settings", onClick: this.showDropdownMenu, 
+             title: __("settings_menu_button_tooltip")}), 
+          React.DOM.ul({className: cx({"dropdown-menu": true, hide: !this.state.showMenu}), 
+              onMouseLeave: this.hideDropdownMenu}, 
+            SettingsDropdownEntry({label: __("settings_menu_item_settings"), 
+                                   onClick: this.handleClickSettingsEntry, 
+                                   icon: "settings"}), 
+            SettingsDropdownEntry({label: __("settings_menu_item_account"), 
+                                   onClick: this.handleClickAccountEntry, 
+                                   icon: "account", 
+                                   displayed: this._isSignedIn()}), 
+            SettingsDropdownEntry({label: this._isSignedIn() ?
+                                          __("settings_menu_item_signout") :
+                                          __("settings_menu_item_signin"), 
+                                   onClick: this.handleClickAuthEntry, 
+                                   icon: this._isSignedIn() ? "signout" : "signin"})
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * Panel layout.
+   */
   var PanelLayout = React.createClass({displayName: 'PanelLayout',
     propTypes: {
       summary: React.PropTypes.string.isRequired
     },
 
     render: function() {
       return (
         React.DOM.div({className: "share generate-url"}, 
@@ -202,18 +313,18 @@ loop.panel = (function(_, mozL10n) {
           this.props.notifier.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     _generateMailTo: function() {
       return encodeURI([
-        "mailto:?subject=" + __("share_email_subject2") + "&",
-        "body=" + __("share_email_body2", {callUrl: this.state.callUrl})
+        "mailto:?subject=" + __("share_email_subject3") + "&",
+        "body=" + __("share_email_body3", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
     handleEmailButtonClick: function(event) {
       // Note: side effect
       document.location = event.target.dataset.mailto;
     },
 
@@ -255,42 +366,59 @@ loop.panel = (function(_, mozL10n) {
             )
           )
         )
       );
     }
   });
 
   /**
+   * FxA sign in/up link component.
+   */
+  var AuthLink = React.createClass({displayName: 'AuthLink',
+    handleSignUpLinkClick: function() {
+      navigator.mozLoop.logInToFxA();
+    },
+
+    render: function() {
+      if (navigator.mozLoop.loggedInToFxA) { // XXX to be implemented
+        return null;
+      }
+      return (
+        React.DOM.p({className: "signin-link"}, 
+          React.DOM.a({href: "#", onClick: this.handleSignUpLinkClick}, 
+            __("panel_footer_signin_or_signup_link")
+          )
+        )
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string
     },
 
-    handleSignUpLinkClick: function() {
-      navigator.mozLoop.logInToFxA();
-    },
-
     render: function() {
       return (
         React.DOM.div(null, 
           CallUrlResult({client: this.props.client, 
                          notifier: this.props.notifier, 
                          callUrl: this.props.callUrl}), 
           ToSView(null), 
           React.DOM.div({className: "footer"}, 
             AvailabilityDropdown(null), 
-            React.DOM.a({className: "signin-link", href: "#", onClick: this.handleSignUpLinkClick}, 
-              __("panel_footer_signin_or_signup_link")
-            )
+            AuthLink(null), 
+            SettingsDropdown(null)
           )
         )
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
@@ -368,24 +496,26 @@ loop.panel = (function(_, mozL10n) {
 
     router = new PanelRouter({
       document: document,
       notifier: new sharedViews.NotificationListView({el: "#messages"})
     });
     Backbone.history.start();
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
+    document.body.setAttribute("dir", mozL10n.getDirection());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
+    SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -17,32 +17,55 @@ loop.panel = (function(_, mozL10n) {
 
   /**
    * Panel router.
    * @type {loop.desktopRouter.DesktopRouter}
    */
   var router;
 
   /**
-   * Availability drop down menu subview.
+   * Dropdown menu mixin.
+   * @type {Object}
    */
-  var AvailabilityDropdown = React.createClass({
+  var DropdownMenuMixin = {
     getInitialState: function() {
-      return {
-        doNotDisturb: navigator.mozLoop.doNotDisturb,
-        showMenu: false
-      };
+      return {showMenu: false};
+    },
+
+    _onBodyClick: function() {
+      this.setState({showMenu: false});
+    },
+
+    componentDidMount: function() {
+      document.body.addEventListener("click", this._onBodyClick);
+    },
+
+    componentWillUnmount: function() {
+      document.body.removeEventListener("click", this._onBodyClick);
     },
 
     showDropdownMenu: function() {
       this.setState({showMenu: true});
     },
 
     hideDropdownMenu: function() {
       this.setState({showMenu: false});
+    }
+  };
+
+  /**
+   * Availability drop down menu subview.
+   */
+  var AvailabilityDropdown = React.createClass({
+    mixins: [DropdownMenuMixin],
+
+    getInitialState: function() {
+      return {
+        doNotDisturb: navigator.mozLoop.doNotDisturb
+      };
     },
 
     // XXX target event can either be the li, the span or the i tag
     // this makes it easier to figure out the target by making a
     // closure with the desired status already passed in.
     changeAvailability: function(newAvailabilty) {
       return function(event) {
         // Note: side effect!
@@ -64,38 +87,38 @@ loop.panel = (function(_, mozL10n) {
       // XXX https://github.com/facebook/react/issues/310 for === htmlFor
       var cx = React.addons.classSet;
       var availabilityStatus = cx({
         'status': true,
         'status-dnd': this.state.doNotDisturb,
         'status-available': !this.state.doNotDisturb
       });
       var availabilityDropdown = cx({
-        'dnd-menu': true,
+        'dropdown-menu': true,
         'hide': !this.state.showMenu
       });
       var availabilityText = this.state.doNotDisturb ?
                               __("display_name_dnd_status") :
                               __("display_name_available_status");
 
       return (
-        <div className="do-not-disturb">
+        <div className="dropdown">
           <p className="dnd-status" onClick={this.showDropdownMenu}>
             <span>{availabilityText}</span>
             <i className={availabilityStatus}></i>
           </p>
           <ul className={availabilityDropdown}
               onMouseLeave={this.hideDropdownMenu}>
             <li onClick={this.changeAvailability("available")}
-                className="dnd-menu-item dnd-make-available">
+                className="dropdown-menu-item dnd-make-available">
               <i className="status status-available"></i>
               <span>{__("display_name_available_status")}</span>
             </li>
             <li onClick={this.changeAvailability("do-not-disturb")}
-                className="dnd-menu-item dnd-make-unavailable">
+                className="dropdown-menu-item dnd-make-unavailable">
               <i className="status status-dnd"></i>
               <span>{__("display_name_dnd_status")}</span>
             </li>
           </ul>
         </div>
       );
     }
   });
@@ -104,17 +127,18 @@ loop.panel = (function(_, mozL10n) {
     getInitialState: function() {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
         var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
-        var tosHTML = __("legal_text_and_links2", {
+        var tosHTML = __("legal_text_and_links3", {
+          "clientShortname": __("client_shortname_fallback"),
           "terms_of_use": React.renderComponentToStaticMarkup(
             <a href={terms_of_use_url} target="_blank">
               {__("legal_text_tos")}
             </a>
           ),
           "privacy_notice": React.renderComponentToStaticMarkup(
             <a href={privacy_notice_url} target="_blank">
               {__("legal_text_privacy")}
@@ -124,16 +148,103 @@ loop.panel = (function(_, mozL10n) {
         return <p className="terms-service"
                   dangerouslySetInnerHTML={{__html: tosHTML}}></p>;
       } else {
         return <div />;
       }
     }
   });
 
+  /**
+   * Panel settings (gear) menu entry.
+   */
+  var SettingsDropdownEntry = React.createClass({
+    propTypes: {
+      onClick: React.PropTypes.func.isRequired,
+      label: React.PropTypes.string.isRequired,
+      icon: React.PropTypes.string,
+      displayed: React.PropTypes.bool
+    },
+
+    getDefaultProps: function() {
+      return {displayed: true};
+    },
+
+    render: function() {
+      if (!this.props.displayed) {
+        return null;
+      }
+      return (
+        <li onClick={this.props.onClick} className="dropdown-menu-item">
+          {this.props.icon ?
+            <i className={"icon icon-" + this.props.icon}></i> :
+            null}
+          <span>{this.props.label}</span>
+        </li>
+      );
+    }
+  });
+
+  /**
+   * Panel settings (gear) menu.
+   */
+  var SettingsDropdown = React.createClass({
+    mixins: [DropdownMenuMixin],
+
+    handleClickSettingsEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAccountEntry: function() {
+      // XXX to be implemented
+    },
+
+    handleClickAuthEntry: function() {
+      if (this._isSignedIn()) {
+        // XXX to be implemented - bug 979845
+        navigator.mozLoop.logOutFromFxA();
+      } else {
+        navigator.mozLoop.logInToFxA();
+      }
+    },
+
+    _isSignedIn: function() {
+      // XXX to be implemented - bug 979845
+      return !!navigator.mozLoop.loggedInToFxA;
+    },
+
+    render: function() {
+      var cx = React.addons.classSet;
+      return (
+        <div className="settings-menu dropdown">
+          <a className="btn btn-settings" onClick={this.showDropdownMenu}
+             title={__("settings_menu_button_tooltip")} />
+          <ul className={cx({"dropdown-menu": true, hide: !this.state.showMenu})}
+              onMouseLeave={this.hideDropdownMenu}>
+            <SettingsDropdownEntry label={__("settings_menu_item_settings")}
+                                   onClick={this.handleClickSettingsEntry}
+                                   icon="settings" />
+            <SettingsDropdownEntry label={__("settings_menu_item_account")}
+                                   onClick={this.handleClickAccountEntry}
+                                   icon="account"
+                                   displayed={this._isSignedIn()} />
+            <SettingsDropdownEntry label={this._isSignedIn() ?
+                                          __("settings_menu_item_signout") :
+                                          __("settings_menu_item_signin")}
+                                   onClick={this.handleClickAuthEntry}
+                                   icon={this._isSignedIn() ? "signout" : "signin"} />
+          </ul>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Panel layout.
+   */
   var PanelLayout = React.createClass({
     propTypes: {
       summary: React.PropTypes.string.isRequired
     },
 
     render: function() {
       return (
         <div className="share generate-url">
@@ -202,18 +313,18 @@ loop.panel = (function(_, mozL10n) {
           this.props.notifier.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     _generateMailTo: function() {
       return encodeURI([
-        "mailto:?subject=" + __("share_email_subject2") + "&",
-        "body=" + __("share_email_body2", {callUrl: this.state.callUrl})
+        "mailto:?subject=" + __("share_email_subject3") + "&",
+        "body=" + __("share_email_body3", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
     handleEmailButtonClick: function(event) {
       // Note: side effect
       document.location = event.target.dataset.mailto;
     },
 
@@ -255,42 +366,59 @@ loop.panel = (function(_, mozL10n) {
             </p>
           </div>
         </PanelLayout>
       );
     }
   });
 
   /**
+   * FxA sign in/up link component.
+   */
+  var AuthLink = React.createClass({
+    handleSignUpLinkClick: function() {
+      navigator.mozLoop.logInToFxA();
+    },
+
+    render: function() {
+      if (navigator.mozLoop.loggedInToFxA) { // XXX to be implemented
+        return null;
+      }
+      return (
+        <p className="signin-link">
+          <a href="#" onClick={this.handleSignUpLinkClick}>
+            {__("panel_footer_signin_or_signup_link")}
+          </a>
+        </p>
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string
     },
 
-    handleSignUpLinkClick: function() {
-      navigator.mozLoop.logInToFxA();
-    },
-
     render: function() {
       return (
         <div>
           <CallUrlResult client={this.props.client}
                          notifier={this.props.notifier}
                          callUrl={this.props.callUrl} />
           <ToSView />
           <div className="footer">
             <AvailabilityDropdown />
-            <a className="signin-link" href="#" onClick={this.handleSignUpLinkClick}>
-              {__("panel_footer_signin_or_signup_link")}
-            </a>
+            <AuthLink />
+            <SettingsDropdown />
           </div>
         </div>
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
@@ -368,24 +496,26 @@ loop.panel = (function(_, mozL10n) {
 
     router = new PanelRouter({
       document: document,
       notifier: new sharedViews.NotificationListView({el: "#messages"})
     });
     Backbone.history.start();
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
+    document.body.setAttribute("dir", mozL10n.getDirection());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
     PanelRouter: PanelRouter,
+    SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -69,62 +69,69 @@
 }
 
 /* Specific cases */
 
 .panel #messages .alert {
   margin-bottom: 0;
 }
 
+/* Dropdown menu (shared styles) */
+
+.dropdown {
+  position: relative;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: -28px;
+  left: 0;
+  background: #fdfdfd;
+  box-shadow: 0 1px 3px rgba(0,0,0,.3);
+  list-style: none;
+  padding: 5px;
+  border-radius: 2px;
+}
+
+body[dir=rtl] .dropdown-menu-item {
+  left: auto;
+  right: 10px;
+}
+
+.dropdown-menu-item {
+  text-align: start;
+  margin: .3em 0;
+  padding: .2em .5em;
+  cursor: pointer;
+  border: 1px solid transparent;
+  border-radius: 2px;
+  font-size: 1em;
+  white-space: nowrap;
+}
+
+.dropdown-menu-item:hover {
+  border: 1px solid #ccc;
+  background: #eee;
+}
+
 /* DnD menu */
 
 .dnd-status {
   border: 1px solid transparent;
   padding: 2px 4px;
   font-size: .9em;
   cursor: pointer;
   border-radius: 3px;
 }
 
 .dnd-status:hover {
   border: 1px solid #DDD;
   background: #F1F1F1;
 }
 
-.do-not-disturb {
-  position: relative;
-}
-
-.dnd-menu {
-  position: absolute;
-  top: -28px;
-  left: 0;
-  background: #fdfdfd;
-  box-shadow: 0 1px 3px rgba(0,0,0,.3);
-  list-style: none;
-  padding: 5px;
-  border-radius: 2px;
-}
-
-.dnd-menu-item {
-  text-align: left;
-  margin: .3em 0;
-  padding: .2em .5em;
-  cursor: pointer;
-  border: 1px solid transparent;
-  border-radius: 2px;
-  font-size: 1em;
-  white-space: nowrap;
-}
-
-.dnd-menu-item:hover {
-  border: 1px solid #ccc;
-  background: #eee;
-}
-
 /* Status badges -- Available/Unavailable */
 
 .status {
   display: inline-block;
   width: 8px;
   height: 8px;
   margin: 0 5px;
   border-radius: 50%;
@@ -136,22 +143,78 @@
 
 .status-dnd {
   border: 1px solid #888;
 }
 
 /* Sign in/up link */
 
 .signin-link {
-  display: none; /* XXX This should be removed as soon bugs 1047144 & 979845 land */
-  line-height: 100%;
+  display: none; /* XXX This should be displayed as soon bug 979845 lands */
+  flex: 2 1 auto;
+  margin-top: 14px;
+  border-right: 1px solid #aaa;
+  padding-right: 1em;
+  margin-right: 1em;
+  text-align: right;
+}
+
+.signin-link a {
   font-size: .9em;
   text-decoration: none;
   color: #888;
-  margin-top: 16px;
+}
+
+/* Settings (gear) menu */
+
+.btn-settings {
+  display: none; /* XXX This should be displayed as soon bug 979845 lands */
+  background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
+  background-size: contain;
+  width: 12px;
+  height: 12px;
+}
+
+.footer .btn-settings {
+  margin-top: 17px; /* used to align the gear icon with the availability dropdown menu inner text */
+  opacity: .6;      /* used to "grey" the icon a little */
+}
+
+.settings-menu .dropdown-menu {
+  /* The panel can't have dropdown menu overflowing its iframe boudaries;
+     let's anchor it from the bottom-right, while resetting the top & left values
+     set by .dropdown-menu */
+  top: auto;
+  left: auto;
+  bottom: -8px;
+  right: 14px;
+}
+
+.settings-menu .icon {
+  display: inline-block;
+  background-size: contain;
+  width: 12px;
+  height: 12px;
+  margin-right: 1em;
+}
+
+.settings-menu .icon-settings {
+  background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center;
+}
+
+.settings-menu .icon-account {
+  background: transparent url(../img/svg/glyph-account-16x16.svg) no-repeat center center;
+}
+
+.settings-menu .icon-signin {
+  background: transparent url(../img/svg/glyph-signin-16x16.svg) no-repeat center center;
+}
+
+.settings-menu .icon-signout {
+  background: transparent url(../img/svg/glyph-signout-16x16.svg) no-repeat center center;
 }
 
 /* Terms of Service */
 
 .terms-service {
   padding: 3px 10px 10px;
   background: #FFF;
   text-align: center;
@@ -176,9 +239,8 @@
   align-items: flex-start;
   font-size: 1em;
   border-top: 1px solid #D1D1D1;
   background: #EAEAEA;
   color: #7F7F7F;
   padding: 14px;
   margin-top: 14px;
 }
-
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-account-16x16.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<g id="Contacts">
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M8,6.526c1.802,0,3.263-1.461,3.263-3.263
+		C11.263,1.461,9.802,0,8,0C6.198,0,4.737,1.461,4.737,3.263C4.737,5.066,6.198,6.526,8,6.526z M14.067,11.421c0,0,0-0.001,0-0.001
+		c0-1.676-1.397-3.119-3.419-3.807L8.001,10.26L5.354,7.613C3.331,8.3,1.933,9.744,1.933,11.42v0.001H1.93
+		c0,1.679,0.328,3.246,0.896,4.579h10.348c0.568-1.333,0.896-2.9,0.896-4.579H14.067z"/>
+</g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-settings-16x16.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<path id="Setting" fill-rule="evenodd" clip-rule="evenodd" fill="#131311" d="M14.77,8c0,0.804,0.262,1.548,0.634,1.678L16,9.887
+	c-0.205,0.874-0.553,1.692-1.011,2.434l-0.567-0.272c-0.355-0.171-1.066,0.17-1.635,0.738c-0.569,0.569-0.909,1.279-0.738,1.635
+	l0.273,0.568c-0.741,0.46-1.566,0.79-2.438,0.998l-0.205-0.584c-0.13-0.372-0.874-0.634-1.678-0.634s-1.548,0.262-1.678,0.634
+	l-0.209,0.596c-0.874-0.205-1.692-0.553-2.434-1.011l0.272-0.567c0.171-0.355-0.17-1.066-0.739-1.635
+	c-0.568-0.568-1.279-0.909-1.635-0.738l-0.568,0.273c-0.46-0.741-0.79-1.566-0.998-2.439l0.584-0.205
+	C0.969,9.547,1.231,8.804,1.231,8c0-0.804-0.262-1.548-0.634-1.678L0,6.112c0.206-0.874,0.565-1.685,1.025-2.427l0.554,0.266
+	c0.355,0.171,1.066-0.17,1.635-0.738c0.569-0.568,0.909-1.28,0.739-1.635L3.686,1.025c0.742-0.46,1.553-0.818,2.427-1.024
+	l0.209,0.596C6.453,0.969,7.197,1.23,8.001,1.23s1.548-0.262,1.678-0.634l0.209-0.596c0.874,0.205,1.692,0.553,2.434,1.011
+	l-0.272,0.567c-0.171,0.355,0.17,1.066,0.738,1.635c0.569,0.568,1.279,0.909,1.635,0.738l0.568-0.273
+	c0.46,0.741,0.79,1.566,0.998,2.438l-0.584,0.205C15.032,6.452,14.77,7.196,14.77,8z M8.001,3.661C5.604,3.661,3.661,5.603,3.661,8
+	c0,2.397,1.943,4.34,4.339,4.34c2.397,0,4.339-1.943,4.339-4.34C12.34,5.603,10.397,3.661,8.001,3.661z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-signin-16x16.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<g id="Outgoing_14x14_1_">
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M9.921,8.415c0.105-0.11,0.146-0.265,0.13-0.432
+		c0.016-0.166-0.025-0.321-0.13-0.429L9.305,6.938l-2.6-2.65C6.402,3.973,5.973,3.906,5.748,4.139L5.238,4.68
+		c-0.225,0.233-0.16,0.679,0.144,0.995L6.44,6.754H0.608C0.272,6.754,0,7.026,0,7.361l0,1.215c0,0.335,0.272,0.607,0.608,0.607H6.47
+		l-1.136,1.155c-0.305,0.313-0.369,0.756-0.144,0.987L5.7,11.861c0.225,0.233,0.654,0.166,0.959-0.149l2.619-2.663L9.921,8.415z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M14,0H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H12.5C13.328,2,14,2.672,14,3.5v9c0,0.828-0.672,1.5-1.5,1.5H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H14c1.105,0,2-0.895,2-2V2C16,0.895,15.105,0,14,0z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-signout-16x16.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M14,0H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H12.5C13.328,2,14,2.672,14,3.5v9c0,0.828-0.672,1.5-1.5,1.5H5.558c-0.331,0-0.6,0.269-0.6,0.6v0.8
+	c0,0.331,0.269,0.6,0.6,0.6H14c1.105,0,2-0.895,2-2V2C16,0.895,15.105,0,14,0z"/>
+<g id="Outgoing_14x14_2_">
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M0.133,7.585c-0.105,0.11-0.146,0.265-0.13,0.432
+		c-0.016,0.166,0.025,0.321,0.13,0.429l0.616,0.615l2.6,2.65c0.304,0.315,0.732,0.382,0.958,0.149l0.51-0.541
+		c0.225-0.233,0.16-0.679-0.144-0.995L3.615,9.246h5.832c0.335,0,0.608-0.272,0.608-0.607V7.424c0-0.335-0.272-0.607-0.608-0.607
+		H3.585L4.72,5.662c0.305-0.313,0.369-0.756,0.144-0.987L4.355,4.139C4.13,3.906,3.701,3.973,3.396,4.287L0.777,6.951L0.133,7.585z"
+		/>
+</g>
+</svg>
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -179,17 +179,17 @@ loop.shared.views = (function(_, OT, l10
 
     render: function() {
       /* jshint ignore:start */
       return (
         React.DOM.ul({className: "conversation-toolbar"}, 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
             React.DOM.button({className: "btn btn-hangup", onClick: this.handleClickHangup, 
                     title: l10n.get("hangup_button_title")}, 
-              l10n.get("hangup_button_caption")
+              l10n.get("hangup_button_caption2")
             )
           ), 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
             MediaControlButton({action: this.handleToggleVideo, 
                                 enabled: this.props.video.enabled, 
                                 scope: "local", type: "video"})
           ), 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
@@ -606,17 +606,17 @@ loop.shared.views = (function(_, OT, l10
         case "form":
           return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient, 
                                sendFeedback: this.sendFeedback, 
                                reset: this.reset, 
                                pending: this.state.pending});
         default:
           return (
             FeedbackLayout({title: 
-              l10n.get("feedback_call_experience_heading")}, 
+              l10n.get("feedback_call_experience_heading2")}, 
               React.DOM.div({className: "faces"}, 
                 React.DOM.button({className: "face face-happy", 
                         onClick: this.handleHappyClick}), 
                 React.DOM.button({className: "face face-sad", 
                         onClick: this.handleSadClick})
               )
             )
           );
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -179,17 +179,17 @@ loop.shared.views = (function(_, OT, l10
 
     render: function() {
       /* jshint ignore:start */
       return (
         <ul className="conversation-toolbar">
           <li className="conversation-toolbar-btn-box">
             <button className="btn btn-hangup" onClick={this.handleClickHangup}
                     title={l10n.get("hangup_button_title")}>
-              {l10n.get("hangup_button_caption")}
+              {l10n.get("hangup_button_caption2")}
             </button>
           </li>
           <li className="conversation-toolbar-btn-box">
             <MediaControlButton action={this.handleToggleVideo}
                                 enabled={this.props.video.enabled}
                                 scope="local" type="video" />
           </li>
           <li className="conversation-toolbar-btn-box">
@@ -606,17 +606,17 @@ loop.shared.views = (function(_, OT, l10
         case "form":
           return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
                                sendFeedback={this.sendFeedback}
                                reset={this.reset}
                                pending={this.state.pending} />;
         default:
           return (
             <FeedbackLayout title={
-              l10n.get("feedback_call_experience_heading")}>
+              l10n.get("feedback_call_experience_heading2")}>
               <div className="faces">
                 <button className="face face-happy"
                         onClick={this.handleHappyClick}></button>
                 <button className="face face-sad"
                         onClick={this.handleSadClick}></button>
               </div>
             </FeedbackLayout>
           );
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -33,18 +33,22 @@ browser.jar:
   content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
   content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
   content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
   content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)
   content/browser/loop/shared/img/video-inverse-14x14.png       (content/shared/img/video-inverse-14x14.png)
   content/browser/loop/shared/img/video-inverse-14x14@2x.png    (content/shared/img/video-inverse-14x14@2x.png)
-  content/browser/loop/shared/img/dropdown-inverse.png       (content/shared/img/dropdown-inverse.png)
-  content/browser/loop/shared/img/dropdown-inverse@2x.png    (content/shared/img/dropdown-inverse@2x.png)
+  content/browser/loop/shared/img/dropdown-inverse.png          (content/shared/img/dropdown-inverse.png)
+  content/browser/loop/shared/img/dropdown-inverse@2x.png       (content/shared/img/dropdown-inverse@2x.png)
+  content/browser/loop/shared/img/svg/glyph-settings-16x16.svg  (content/shared/img/svg/glyph-settings-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-account-16x16.svg   (content/shared/img/svg/glyph-account-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-signin-16x16.svg    (content/shared/img/svg/glyph-signin-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-signout-16x16.svg   (content/shared/img/svg/glyph-signout-16x16.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js            (content/shared/js/models.js)
   content/browser/loop/shared/js/router.js            (content/shared/js/router.js)
   content/browser/loop/shared/js/views.js             (content/shared/js/views.js)
   content/browser/loop/shared/js/utils.js             (content/shared/js/utils.js)
   content/browser/loop/shared/js/websocket.js         (content/shared/js/websocket.js)
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -50,18 +50,16 @@ body,
 
 .container-box {
   display: flex;
   flex-direction: column;
   width: 100%;
   align-content: center;
 }
 
-.footer,
-.footer a,
 .terms-service,
 .terms-service a {
   font-size: .6rem;
   font-weight: 400;
   color: #adadad;
 }
 
   .terms-service a {
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -68,17 +68,17 @@ loop.webapp = (function($, _, OT, webL10
 
     render: function() {
       /* jshint ignore:start */
       return (
         React.DOM.div({className: "expired-url-info"}, 
           React.DOM.div({className: "info-panel"}, 
             React.DOM.div({className: "firefox-logo"}), 
             React.DOM.h1(null, __("call_url_unavailable_notification_heading")), 
-            React.DOM.h4(null, __("call_url_unavailable_notification_message"))
+            React.DOM.h4(null, __("call_url_unavailable_notification_message2"))
           ), 
           PromoteFirefoxView({helper: this.props.helper})
         )
       );
       /* jshint ignore:end */
     }
   });
 
@@ -245,33 +245,33 @@ loop.webapp = (function($, _, OT, webL10
         /* jshint ignore:start */
         React.DOM.div({className: "container"}, 
           React.DOM.div({className: "container-box"}, 
 
             ConversationHeader({
               urlCreationDateString: this.state.urlCreationDateString}), 
 
             React.DOM.p({className: "standalone-call-btn-label"}, 
-              __("initiate_call_button_label")
+              __("initiate_call_button_label2")
             ), 
 
             React.DOM.div({id: "messages"}), 
 
             React.DOM.div({className: "btn-group"}, 
               React.DOM.div({className: "flex-padding-1"}), 
               React.DOM.div({className: "standalone-btn-chevron-menu-group"}, 
                 React.DOM.div({className: "btn-group-chevron"}, 
                   React.DOM.div({className: "btn-group"}, 
 
                     React.DOM.button({className: btnClassStartCall, 
                             onClick: this._initiateOutgoingCall("audio-video"), 
                             disabled: this.state.disableCallButton, 
-                            title: __("initiate_audio_video_call_tooltip")}, 
+                            title: __("initiate_audio_video_call_tooltip2")}, 
                       React.DOM.span({className: "standalone-call-btn-text"}, 
-                        __("initiate_audio_video_call_button")
+                        __("initiate_audio_video_call_button2")
                       ), 
                       React.DOM.span({className: "standalone-call-btn-video-icon"})
                     ), 
 
                     React.DOM.div({className: "btn-chevron", 
                       onClick: this._toggleCallOptionsMenu}
                     )
 
@@ -280,17 +280,17 @@ loop.webapp = (function($, _, OT, webL10
                   React.DOM.ul({className: dropdownMenuClasses}, 
                     React.DOM.li(null, 
                       /*
                        Button required for disabled state.
                        */
                       React.DOM.button({className: "start-audio-only-call", 
                               onClick: this._initiateOutgoingCall("audio"), 
                               disabled: this.state.disableCallButton}, 
-                        __("initiate_audio_call_button")
+                        __("initiate_audio_call_button2")
                       )
                     )
                   )
 
                 )
               ), 
               React.DOM.div({className: "flex-padding-1"})
             ), 
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -68,17 +68,17 @@ loop.webapp = (function($, _, OT, webL10
 
     render: function() {
       /* jshint ignore:start */
       return (
         <div className="expired-url-info">
           <div className="info-panel">
             <div className="firefox-logo" />
             <h1>{__("call_url_unavailable_notification_heading")}</h1>
-            <h4>{__("call_url_unavailable_notification_message")}</h4>
+            <h4>{__("call_url_unavailable_notification_message2")}</h4>
           </div>
           <PromoteFirefoxView helper={this.props.helper} />
         </div>
       );
       /* jshint ignore:end */
     }
   });
 
@@ -245,33 +245,33 @@ loop.webapp = (function($, _, OT, webL10
         /* jshint ignore:start */
         <div className="container">
           <div className="container-box">
 
             <ConversationHeader
               urlCreationDateString={this.state.urlCreationDateString} />
 
             <p className="standalone-call-btn-label">
-              {__("initiate_call_button_label")}
+              {__("initiate_call_button_label2")}
             </p>
 
             <div id="messages"></div>
 
             <div className="btn-group">
               <div className="flex-padding-1"></div>
               <div className="standalone-btn-chevron-menu-group">
                 <div className="btn-group-chevron">
                   <div className="btn-group">
 
                     <button className={btnClassStartCall}
                             onClick={this._initiateOutgoingCall("audio-video")}
                             disabled={this.state.disableCallButton}
-                            title={__("initiate_audio_video_call_tooltip")} >
+                            title={__("initiate_audio_video_call_tooltip2")} >
                       <span className="standalone-call-btn-text">
-                        {__("initiate_audio_video_call_button")}
+                        {__("initiate_audio_video_call_button2")}
                       </span>
                       <span className="standalone-call-btn-video-icon"></span>
                     </button>
 
                     <div className="btn-chevron"
                       onClick={this._toggleCallOptionsMenu}>
                     </div>
 
@@ -280,17 +280,17 @@ loop.webapp = (function($, _, OT, webL10
                   <ul className={dropdownMenuClasses}>
                     <li>
                       {/*
                        Button required for disabled state.
                        */}
                       <button className="start-audio-only-call"
                               onClick={this._initiateOutgoingCall("audio")}
                               disabled={this.state.disableCallButton} >
-                        {__("initiate_audio_call_button")}
+                        {__("initiate_audio_call_button2")}
                       </button>
                     </li>
                   </ul>
 
                 </div>
               </div>
               <div className="flex-padding-1"></div>
             </div>
--- a/browser/components/loop/standalone/content/l10n/data.ini
+++ b/browser/components/loop/standalone/content/l10n/data.ini
@@ -1,64 +1,71 @@
 ## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
 [en]
-call_has_ended=Your call has ended.
+restart_call=Rejoin
+conversation_has_ended=Your conversation has ended.
 call_timeout_notification_text=Your call did not go through.
 missing_conversation_info=Missing conversation information.
 network_disconnected=The network connection terminated abruptly.
 peer_ended_conversation2=The person you were calling has ended the conversation.
+connection_error_see_console_notification=Call failed; see console for details.
+generic_failure_title=Something went wrong.
+generic_failure_with_reason2=You can try again or email a link to be reached at later.
+generic_failure_no_reason2=Would you like to try again?
+retry_call_button=Retry
+feedback_report_user_button=Report User
 unable_retrieve_call_info=Unable to retrieve conversation information.
 hangup_button_title=Hang up
-hangup_button_caption=End Call
+hangup_button_caption2=Exit
 mute_local_audio_button_title=Mute your audio
 unmute_local_audio_button_title=Unmute your audio
 mute_local_video_button_title=Mute your video
 unmute_local_video_button_title=Unmute your video
-start_call=Start the call
+outgoing_call_title=Start conversation?
+call_with_contact_title=Conversation with {{incomingCallIdentity}}
 welcome=Welcome to the {{clientShortname}} web client.
 incompatible_browser=Incompatible Browser
 powered_by_webrtc=The audio and video components of {{clientShortname}} are powered by WebRTC.
 use_latest_firefox.innerHTML=Please try this link in a WebRTC-enabled browser, such as <a href="{{ff_url}}">{{brandShortname}}</a>.
 incompatible_device=Incompatible device
 sorry_device_unsupported=Sorry, {{clientShortname}} does not currently support your device.
 use_firefox_windows_mac_linux=Please open this page using the latest {{brandShortname}} on Windows, Android, Mac or Linux.
 connection_error_see_console_notification=Call failed; see console for details.
 call_url_unavailable_notification_heading=Oops!
-call_url_unavailable_notification_message=This URL is unavailable.
+call_url_unavailable_notification_message2=Sorry, this URL is not available. It may be expired or entered incorrectly.
 promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
 get_firefox_button=Get {{brandShortname}}
-call_url_unavailable_notification=This URL is unavailable.
-initiate_call_button_label=Click Call to start a video chat
-initiate_audio_video_call_button=Call
-initiate_audio_video_call_tooltip=Start a video call
-initiate_audio_call_button=Voice call
+initiate_call_button_label2=Ready to start your conversation?
+initiate_audio_video_call_button2=Start
+initiate_audio_video_call_tooltip2=Start a video conversation
+initiate_audio_call_button2=Voice conversation
+reject_incoming_call=Cancel
 legal_text_and_links=By using this product you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
 terms_of_use_link_text=Terms of use
 privacy_notice_link_text=Privacy notice
 brandShortname=Firefox
 clientShortname=WebRTC!
 ## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
 call_url_creation_date_label=(from {{call_url_creation_date}})
+call_progress_connecting_description=Connecting…
+call_progress_ringing_description=Ringing…
 
 [fr]
-call_has_ended=L'appel est terminé.
 call_timeout_notification_text=Votre appel n'a pas abouti.
 missing_conversation_info=Informations de communication manquantes.
 network_disconnected=La connexion réseau semble avoir été interrompue.
 unable_retrieve_call_info=Impossible de récupérer les informations liées à cet appel.
 hangup_button_title=Terminer l'appel
 hangup_button_caption=Raccrocher
 mute_local_audio_button_title=Couper la diffusion audio
 unmute_local_audio_button_title=Reprendre la diffusion audio
 mute_local_video_button_title=Couper la diffusion vidéo
 unmute_local_video_button_title=Reprendre la diffusion vidéo
-start_call=Démarrer l'appel
 welcome=Bienvenue sur {{clientShortname}}.
 incompatible_browser=Navigateur non supporté
 powered_by_webrtc=Les fonctionnalités audio et vidéo de {{clientShortname}} utilisent WebRTC.
 use_latest_firefox.innerHTML=Veuillez essayer ce lien dans un navigateur acceptant WebRTC, par exemple <a href="{{ff_url}}">{{brandShortname}}</a>.
 incompatible_device=Plateforme non supportée
 sorry_device_unsupported=Désolé, {{clientShortname}} ne fonctionne actuellement pas sur votre appareil.
 use_firefox_windows_mac_linux=Merci d'ouvrir cette page avec une version récente de {{brandShortname}} pour Windows, Android, Mac ou Linux.
 call_url_unavailable_notification_heading=Oups !
-call_url_unavailable_notification_message=Cette URL n'est pas disponible.
 promote_firefox_hello_heading=Téléchargez {{brandShortname}} pour passer des appels audio et vidéo gratuitement !
 get_firefox_button=Téléchargez {{brandShortname}}
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -212,28 +212,100 @@ describe("loop.panel", function() {
       };
 
       view = TestUtils.renderIntoDocument(loop.panel.PanelView({
         notifier: notifier,
         client: fakeClient
       }));
     });
 
-    describe("FxA sign in/up link", function() {
+    describe("AuthLink", function() {
       it("should trigger the FxA sign in/up process when clicking the link",
         function() {
+          navigator.mozLoop.loggedInToFxA = false;
           navigator.mozLoop.logInToFxA = sandbox.stub();
 
           TestUtils.Simulate.click(
-            view.getDOMNode().querySelector(".signin-link"));
+            view.getDOMNode().querySelector(".signin-link a"));
 
           sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
         });
       });
 
+    describe("SettingsDropdown", function() {
+      var view;
+
+      beforeEach(function() {
+        navigator.mozLoop.logInToFxA = sandbox.stub();
+        navigator.mozLoop.logOutFromFxA = sandbox.stub();
+      });
+
+      it("should show a signin entry when user is not authenticated",
+        function() {
+          navigator.mozLoop.loggedInToFxA = false;
+
+          var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+          expect(view.getDOMNode().querySelectorAll(".icon-signout"))
+            .to.have.length.of(0);
+          expect(view.getDOMNode().querySelectorAll(".icon-signin"))
+            .to.have.length.of(1);
+        });
+
+      it("should show a signout entry when user is authenticated", function() {
+        navigator.mozLoop.loggedInToFxA = true;
+
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        expect(view.getDOMNode().querySelectorAll(".icon-signout"))
+          .to.have.length.of(1);
+        expect(view.getDOMNode().querySelectorAll(".icon-signin"))
+          .to.have.length.of(0);
+      });
+
+      it("should show an account entry when user is authenticated", function() {
+        navigator.mozLoop.loggedInToFxA = true;
+
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        expect(view.getDOMNode().querySelectorAll(".icon-account"))
+          .to.have.length.of(1);
+      });
+
+      it("should hide any account entry when user is not authenticated",
+        function() {
+          navigator.mozLoop.loggedInToFxA = false;
+
+          var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+          expect(view.getDOMNode().querySelectorAll(".icon-account"))
+            .to.have.length.of(0);
+        });
+
+      it("should sign in the user on click when unauthenticated", function() {
+        navigator.mozLoop.loggedInToFxA = false;
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        TestUtils.Simulate.click(
+          view.getDOMNode().querySelector(".icon-signin"));
+
+        sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
+      });
+
+      it("should sign out the user on click when authenticated", function() {
+        navigator.mozLoop.loggedInToFxA = true;
+        var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
+
+        TestUtils.Simulate.click(
+          view.getDOMNode().querySelector(".icon-signout"));
+
+        sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
+      });
+    });
+
     describe("#render", function() {
       it("should render a ToSView", function() {
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
       });
     });
   });
 
   describe("loop.panel.CallUrlResult", function() {
@@ -259,19 +331,19 @@ describe("loop.panel", function() {
 
     describe("Rendering the component should generate a call URL", function() {
 
       beforeEach(function() {
         document.mozL10n.initialize({
           getStrings: function(key) {
             var text;
 
-            if (key === "share_email_subject2")
+            if (key === "share_email_subject3")
               text = "email-subject";
-            else if (key === "share_email_body2")
+            else if (key === "share_email_body3")
               text = "{{callUrl}}";
 
             return JSON.stringify({textContent: text});
           }
         });
       });
 
       it("should make a request to requestCallUrl", function() {
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -8,9 +8,10 @@ support-files =
 [browser_fxa_login.js]
 skip-if = !debug
 [browser_loop_fxa_server.js]
 [browser_LoopContacts.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 skip-if = buildapp == 'mulet'
+[browser_toolbarbutton.js]
 [browser_mozLoop_pluralStrings.js]
--- a/browser/components/loop/test/mochitest/browser_LoopContacts.js
+++ b/browser/components/loop/test/mochitest/browser_LoopContacts.js
@@ -141,35 +141,35 @@ registerCleanupFunction(function () {
 
 // Test adding a contact.
 add_task(function* () {
   let contacts = yield promiseLoadContacts();
   for (let i = 0, l = contacts.length; i < l; ++i) {
     compareContacts(contacts[i], kContacts[i]);
   }
 
-  // Add a contact.
+  info("Add a contact.");
   let deferred = Promise.defer();
   gExpectedAdds.push(kDanglingContact);
   LoopContacts.add(kDanglingContact, (err, contact) => {
     Assert.ok(!err, "There shouldn't be an error");
     compareContacts(contact, kDanglingContact);
 
-    // Check if it's persisted.
+    info("Check if it's persisted.");
     LoopContacts.get(contact._guid, (err, contact) => {
       Assert.ok(!err, "There shouldn't be an error");
       compareContacts(contact, kDanglingContact);
       deferred.resolve();
     });
   });
   yield deferred.promise;
 });
 
-// Test removing all contacts.
 add_task(function* () {
+  info("Test removing all contacts.");
   let contacts = yield promiseLoadContacts();
 
   let deferred = Promise.defer();
   LoopContacts.removeAll(function(err) {
     Assert.ok(!err, "There shouldn't be an error");
     LoopContacts.getAll(function(err, found) {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.equal(found.length, 0, "There shouldn't be any contacts left");
@@ -178,100 +178,100 @@ add_task(function* () {
   });
   yield deferred.promise;
 });
 
 // Test retrieving a contact.
 add_task(function* () {
   let contacts = yield promiseLoadContacts();
 
-  // Get a single contact.
+  info("Get a single contact.");
   let deferred = Promise.defer();
   LoopContacts.get(contacts[1]._guid, (err, contact) => {
     Assert.ok(!err, "There shouldn't be an error");
     compareContacts(contact, kContacts[1]);
     deferred.resolve();
   });
   yield deferred.promise;
 
-  // Get a single contact by id.
+  info("Get a single contact by id.");
   let deferred = Promise.defer();
   LoopContacts.getByServiceId(2, (err, contact) => {
     Assert.ok(!err, "There shouldn't be an error");
     compareContacts(contact, kContacts[1]);
     deferred.resolve();
   });
   yield deferred.promise;
 
-  // Get a couple of contacts.
+  info("Get a couple of contacts.");
   let deferred = Promise.defer();
   let toRetrieve = [contacts[0], contacts[2], contacts[3]];
   LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
     Assert.ok(!err, "There shouldn't be an error");
     Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
                  "size as the list of items to retrieve");
     for (let contact of toRetrieve) {
       let found = result.filter(c => c._guid == contact._guid);
       Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
       compareContacts(found[0], contact);
     }
     deferred.resolve();
   });
   yield deferred.promise;
 
-  // Get all contacts.
+  info("Get all contacts.");
   deferred = Promise.defer();
   LoopContacts.getAll((err, contacts) => {
     Assert.ok(!err, "There shouldn't be an error");
     for (let i = 0, l = contacts.length; i < l; ++i) {
       compareContacts(contacts[i], kContacts[i]);
     }
     deferred.resolve();
   });
   yield deferred.promise;
 
-  // Get a non-existent contact.
+  info("Get a non-existent contact.");
   deferred = Promise.defer();
   LoopContacts.get(1000, (err, contact) => {
     Assert.ok(!err, "There shouldn't be an error");
     Assert.ok(!contact, "There shouldn't be a contact");
     deferred.resolve();
   });
   yield deferred.promise;
 });
 
 // Test removing a contact.
 add_task(function* () {
   let contacts = yield promiseLoadContacts();
 
-  // Remove a single contact.
+  info("Remove a single contact.");
   let deferred = Promise.defer();
   let toRemove = contacts[2]._guid;
   gExpectedRemovals.push(toRemove);
   LoopContacts.remove(toRemove, err => {
     Assert.ok(!err, "There shouldn't be an error");
 
     LoopContacts.get(toRemove, (err, contact) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.ok(!contact, "There shouldn't be a contact");
       deferred.resolve();
     });
   });
   yield deferred.promise;
 
-  // Remove a non-existing contact.
+  info("Remove a non-existing contact.");
   deferred = Promise.defer();
   LoopContacts.remove(1000, (err, contact) => {
     Assert.ok(!err, "There shouldn't be an error");
     Assert.ok(!contact, "There shouldn't be a contact");
     deferred.resolve();
   });
   yield deferred.promise;
 
-  // Remove multiple contacts.
+  info("Remove multiple contacts.");
   deferred = Promise.defer();
   toRemove = [contacts[0]._guid, contacts[1]._guid];
   gExpectedRemovals.push(...toRemove);
   LoopContacts.removeMany(toRemove, err => {
     Assert.ok(!err, "There shouldn't be an error");
 
     LoopContacts.getAll((err, contacts) => {
       Assert.ok(!err, "There shouldn't be an error");
@@ -287,39 +287,39 @@ add_task(function* () {
 });
 
 // Test updating a contact.
 add_task(function* () {
   let contacts = yield promiseLoadContacts();
 
   const newBday = (new Date(403920000000)).toISOString();
 
-  // Update a single contact.
+  info("Update a single contact.");
   let deferred = Promise.defer();
   let toUpdate = {
     _guid: contacts[2]._guid,
     bday: newBday
   };
   gExpectedUpdates.push(contacts[2]._guid);
   LoopContacts.update(toUpdate, (err, result) => {
     Assert.ok(!err, "There shouldn't be an error");
     Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
 
     LoopContacts.get(toUpdate._guid, (err, contact) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.equal(contact.bday, newBday, "Birthday should be the same");
-      // Check that all other properties were left intact.
+      info("Check that all other properties were left intact.");
       contacts[2].bday = newBday;
       compareContacts(contact, contacts[2]);
       deferred.resolve();
     });
   });
   yield deferred.promise;
 
-  // Update a non-existing contact.
+  info("Update a non-existing contact.");
   deferred = Promise.defer();
   toUpdate = {
     _guid: 1000,
     bday: newBday
   };
   LoopContacts.update(toUpdate, (err, contact) => {
     Assert.ok(err, "There should be an error");
     Assert.equal(err.message, "Contact with _guid '1000' could not be found",
@@ -328,65 +328,65 @@ add_task(function* () {
   });
   yield deferred.promise;
 });
 
 // Test blocking and unblocking a contact.
 add_task(function* () {
   let contacts = yield promiseLoadContacts();
 
-  // Block contact.
+  info("Block contact.");
   let deferred = Promise.defer();
   let toBlock = contacts[1]._guid;
   gExpectedUpdates.push(toBlock);
   LoopContacts.block(toBlock, (err, result) => {
     Assert.ok(!err, "There shouldn't be an error");
     Assert.equal(result, toBlock, "Result should be the same as the contact ID");
 
     LoopContacts.get(toBlock, (err, contact) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
-      // Check that all other properties were left intact.
+      info("Check that all other properties were left intact.");
       delete contact.blocked;
       compareContacts(contact, contacts[1]);
       deferred.resolve();
     });
   });
   yield deferred.promise;
 
-  // Block a non-existing contact.
+  info("Block a non-existing contact.");
   deferred = Promise.defer();
   LoopContacts.block(1000, err => {
     Assert.ok(err, "There should be an error");
     Assert.equal(err.message, "Contact with _guid '1000' could not be found",
                  "Error message should be correct");
     deferred.resolve();
   });
   yield deferred.promise;
 
-  // Unblock a contact.
+  info("Unblock a contact.");
   deferred = Promise.defer();
   let toUnblock = contacts[1]._guid;
   gExpectedUpdates.push(toUnblock);
   LoopContacts.unblock(toUnblock, (err, result) => {
     Assert.ok(!err, "There shouldn't be an error");
     Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
 
     LoopContacts.get(toUnblock, (err, contact) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
-      // Check that all other properties were left intact.
+      info("Check that all other properties were left intact.");
       delete contact.blocked;
       compareContacts(contact, contacts[1]);
       deferred.resolve();
     });
   });
   yield deferred.promise;
 
-  // Unblock a non-existing contact.
+  info("Unblock a non-existing contact.");
   deferred = Promise.defer();
   LoopContacts.unblock(1000, err => {
     Assert.ok(err, "There should be an error");
     Assert.equal(err.message, "Contact with _guid '1000' could not be found",
                  "Error message should be correct");
     deferred.resolve();
   });
   yield deferred.promise;
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the toolbar button states.
+ */
+
+"use strict";
+
+const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).
+                               MozLoopServiceInternal;
+
+registerCleanupFunction(function*() {
+  MozLoopService.doNotDisturb = false;
+  yield MozLoopServiceInternal.clearError("testing");
+});
+
+add_task(function* test_doNotDisturb() {
+  yield MozLoopService.doNotDisturb = true;
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
+  yield MozLoopService.doNotDisturb = false;
+  Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
+});
+
+add_task(function* test_error() {
+  yield MozLoopServiceInternal.setError("testing", {});
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
+  yield MozLoopServiceInternal.clearError("testing");
+  Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
+});
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -29,30 +29,36 @@
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
     return false;
   }
 
+  function noop(){}
+
   // Feedback API client configured to send data to the stage input server,
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var mockClient = {
-    requestCallUrl: function() {}
+    requestCallUrl: noop,
+    requestCallUrlInfo: noop
   };
 
   var mockConversationModel = new loop.shared.models.ConversationModel({}, {sdk: {}});
 
+  // Fake notifier
+  var mockNotifier = {};
+
   var Example = React.createClass({displayName: 'Example',
     render: function() {
       var cx = React.addons.classSet;
       return (
         React.DOM.div({className: "example"}, 
           React.DOM.h3(null, this.props.summary), 
           React.DOM.div({className: cx({comp: true, dashed: this.props.dashed}), 
                style: this.props.style || {}}, 
@@ -100,98 +106,125 @@
     render: function() {
       return (
         ShowCase(null, 
           Section({name: "PanelView"}, 
             React.DOM.p({className: "note"}, 
               React.DOM.strong(null, "Note:"), " 332px wide."
             ), 
             Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}}, 
-              PanelView({callUrl: "http://invalid.example.url/", client: mockClient})
+              PanelView({client: mockClient, notifier: mockNotifier, 
+                         callUrl: "http://invalid.example.url/"})
             ), 
             Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}}, 
-              PanelView({client: mockClient})
+              PanelView({client: mockClient, notifier: mockNotifier})
             )
           ), 
 
           Section({name: "IncomingCallView"}, 
             Example({summary: "Default", dashed: "true", style: {width: "280px"}}, 
-              IncomingCallView(null)
+              IncomingCallView({model: mockConversationModel})
             )
           ), 
 
           Section({name: "ConversationToolbar"}, 
             React.DOM.h3(null, "Desktop Conversation Window"), 
             React.DOM.div({className: "conversation-window"}, 
               Example({summary: "Default (260x265)", dashed: "true"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Video muted"}, 
-                ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: false}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Audio muted"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: false}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               )
             ), 
 
             React.DOM.h3(null, "Standalone"), 
             React.DOM.div({className: "standalone"}, 
               Example({summary: "Default"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Video muted"}, 
-                ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
+                ConversationToolbar({video: {enabled: false}, 
+                                     audio: {enabled: true}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               ), 
               Example({summary: "Audio muted"}, 
-                ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
+                ConversationToolbar({video: {enabled: true}, 
+                                     audio: {enabled: false}, 
+                                     hangup: noop, 
+                                     publishStream: noop})
               )
             )
           ), 
 
           Section({name: "StartConversationView"}, 
-
             Example({summary: "Start conversation view", dashed: "true"}, 
               React.DOM.div({className: "standalone"}, 
                 StartConversationView({model: mockConversationModel, 
-                  client: mockClient})
+                                       client: mockClient, 
+                                       notifier: mockNotifier})
               )
             )
-
           ), 
 
           Section({name: "ConversationView"}, 
-
             Example({summary: "Desktop conversation window", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "conversation-window"}, 
-                ConversationView({video: {enabled: true}, audio: {enabled: true}, 
-                                  model: mockConversationModel})
+                ConversationView({sdk: {}, 
+                                  model: mockConversationModel, 
+                                  video: {enabled: true}, 
+                                  audio: {enabled: true}})
               )
             ), 
             Example({summary: "Standalone version"}, 
               React.DOM.div({className: "standalone"}, 
-                ConversationView({video: {enabled: true}, audio: {enabled: true}, 
-                                  model: mockConversationModel})
+                ConversationView({sdk: {}, 
+                                  model: mockConversationModel, 
+                                  video: {enabled: true}, 
+                                  audio: {enabled: true}})
               )
+            ), 
+            Example({summary: "Default"}, 
+              ConversationView({sdk: {}, 
+                                model: mockConversationModel, 
+                                video: {enabled: true}, 
+                                audio: {enabled: true}})
             )
           ), 
 
           Section({name: "FeedbackView"}, 
             React.DOM.p({className: "note"}, 
               React.DOM.strong(null, "Note:"), " For the useable demo, you can access submitted data at ", 
               React.DOM.a({href: "https://input.allizom.org/"}, "input.allizom.org"), "."
             ), 
             Example({summary: "Default (useable demo)", dashed: "true", style: {width: "280px"}}, 
               FeedbackView({feedbackApiClient: stageFeedbackApiClient})
             ), 
             Example({summary: "Detailed form", dashed: "true", style: {width: "280px"}}, 
-              FeedbackView({step: "form"})
+              FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "form"})
             ), 
             Example({summary: "Thank you!", dashed: "true", style: {width: "280px"}}, 
-              FeedbackView({step: "finished"})
+              FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "finished"})
             )
           ), 
 
           Section({name: "CallUrlExpiredView"}, 
             Example({summary: "Firefox User"}, 
               CallUrlExpiredView({helper: {isFirefox: returnTrue}})
             ), 
             Example({summary: "Non-Firefox User"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -29,30 +29,36 @@
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
     return false;
   }
 
+  function noop(){}
+
   // Feedback API client configured to send data to the stage input server,
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var mockClient = {
-    requestCallUrl: function() {}
+    requestCallUrl: noop,
+    requestCallUrlInfo: noop
   };
 
   var mockConversationModel = new loop.shared.models.ConversationModel({}, {sdk: {}});
 
+  // Fake notifier
+  var mockNotifier = {};
+
   var Example = React.createClass({
     render: function() {
       var cx = React.addons.classSet;
       return (
         <div className="example">
           <h3>{this.props.summary}</h3>
           <div className={cx({comp: true, dashed: this.props.dashed})}
                style={this.props.style || {}}>
@@ -100,98 +106,125 @@
     render: function() {
       return (
         <ShowCase>
           <Section name="PanelView">
             <p className="note">
               <strong>Note:</strong> 332px wide.
             </p>
             <Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
-              <PanelView callUrl="http://invalid.example.url/" client={mockClient} />
+              <PanelView client={mockClient} notifier={mockNotifier}
+                         callUrl="http://invalid.example.url/" />
             </Example>
             <Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
-              <PanelView client={mockClient} />
+              <PanelView client={mockClient} notifier={mockNotifier} />
             </Example>
           </Section>
 
           <Section name="IncomingCallView">
             <Example summary="Default" dashed="true" style={{width: "280px"}}>
-              <IncomingCallView />
+              <IncomingCallView model={mockConversationModel} />
             </Example>
           </Section>
 
           <Section name="ConversationToolbar">
             <h3>Desktop Conversation Window</h3>
             <div className="conversation-window">
               <Example summary="Default (260x265)" dashed="true">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Video muted">
-                <ConversationToolbar video={{enabled: false}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: false}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Audio muted">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: false}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: false}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
             </div>
 
             <h3>Standalone</h3>
             <div className="standalone">
               <Example summary="Default">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Video muted">
-                <ConversationToolbar video={{enabled: false}} audio={{enabled: true}} />
+                <ConversationToolbar video={{enabled: false}}
+                                     audio={{enabled: true}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
               <Example summary="Audio muted">
-                <ConversationToolbar video={{enabled: true}} audio={{enabled: false}} />
+                <ConversationToolbar video={{enabled: true}}
+                                     audio={{enabled: false}}
+                                     hangup={noop}
+                                     publishStream={noop} />
               </Example>
             </div>
           </Section>
 
           <Section name="StartConversationView">
-
             <Example summary="Start conversation view" dashed="true">
               <div className="standalone">
                 <StartConversationView model={mockConversationModel}
-                  client={mockClient} />
+                                       client={mockClient}
+                                       notifier={mockNotifier} />
               </div>
             </Example>
-
           </Section>
 
           <Section name="ConversationView">
-
             <Example summary="Desktop conversation window" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="conversation-window">
-                <ConversationView video={{enabled: true}} audio={{enabled: true}}
-                                  model={mockConversationModel} />
+                <ConversationView sdk={{}}
+                                  model={mockConversationModel}
+                                  video={{enabled: true}}
+                                  audio={{enabled: true}} />
               </div>
             </Example>
             <Example summary="Standalone version">
               <div className="standalone">
-                <ConversationView video={{enabled: true}} audio={{enabled: true}}
-                                  model={mockConversationModel} />
+                <ConversationView sdk={{}}
+                                  model={mockConversationModel}
+                                  video={{enabled: true}}
+                                  audio={{enabled: true}} />
               </div>
             </Example>
+            <Example summary="Default">
+              <ConversationView sdk={{}}
+                                model={mockConversationModel}
+                                video={{enabled: true}}
+                                audio={{enabled: true}} />
+            </Example>
           </Section>
 
           <Section name="FeedbackView">
             <p className="note">
               <strong>Note:</strong> For the useable demo, you can access submitted data at&nbsp;
               <a href="https://input.allizom.org/">input.allizom.org</a>.
             </p>
             <Example summary="Default (useable demo)" dashed="true" style={{width: "280px"}}>
               <FeedbackView feedbackApiClient={stageFeedbackApiClient} />
             </Example>
             <Example summary="Detailed form" dashed="true" style={{width: "280px"}}>
-              <FeedbackView step="form" />
+              <FeedbackView feedbackApiClient={stageFeedbackApiClient} step="form" />
             </Example>
             <Example summary="Thank you!" dashed="true" style={{width: "280px"}}>
-              <FeedbackView step="finished" />
+              <FeedbackView feedbackApiClient={stageFeedbackApiClient} step="finished" />
             </Example>
           </Section>
 
           <Section name="CallUrlExpiredView">
             <Example summary="Firefox User">
               <CallUrlExpiredView helper={{isFirefox: returnTrue}} />
             </Example>
             <Example summary="Non-Firefox User">
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -1,72 +1,270 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Panel Strings
 
 share_link_header_text=Share this link to invite someone to talk:
 
+## LOCALIZATION NOTE(invitee_name_label): Displayed when obtaining a url.
+## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
+## Click the label icon at the end of the url field.
+invitee_name_label=Who are you inviting?
+## LOCALIZATION NOTE(invitee_expire_days_label): Allows the user to adjust
+## the expiry time. Click the label icon at the end of the url field to see where
+## this is:
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
+## Semicolon-separated list of plural forms. See:
+## http://developer.mozilla.org/en/docs/Localization_and_Plurals
+## In this item, don't translate the part between {{..}}
+invitee_expire_days_label=Invitation will expire in {{expiry_time}} day;Invitation will expire in {{expiry_time}} days
+## LOCALIZATION NOTE(invitee_expire_hours_label): Allows the user to adjust
+## the expiry time. Click the label icon are the end of the url field to see where
+## this is:
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
+## Semicolon-separated list of plural forms. See:
+## http://developer.mozilla.org/en/docs/Localization_and_Plurals
+## In this item, don't translate the part between {{..}}
+invitee_expire_hours_label=Invitation will expire in {{expiry_time}} hour;Invitation will expire in {{expiry_time}} hours
+
 # Status text
+display_name_guest=Guest
 display_name_dnd_status=Do Not Disturb
 display_name_available_status=Available
 
+# Error bars
+## LOCALIZATION NOTE(unable_retrieve_url,session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
+## These may be displayed at the top of the panel here:
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
 unable_retrieve_url=Sorry, we were unable to retrieve a call url.
+session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
+could_not_authenticate=Could Not Authenticate
+password_changed_question=Did you change your password?
+try_again_later=Please try again later
+could_not_connect=Could Not Connect To The Server
+check_internet_connection=Please check your internet connection
+login_expired=Your Login Has Expired
+service_not_available=Service Unavailable At This Time
+problem_accessing_account=There Was A Problem Accessing Your Account
+
+## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
+## the appropriate action.
+## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error for location
+retry_button=Retry
+
+share_email_subject3=You have been invited to a conversation
+## LOCALIZATION NOTE (share_email_body3): In this item, don't translate the
+## part between {{..}} and leave the \r\n\r\n part alone
+share_email_body3=To accept this invitation, just copy or click this link to start your conversation:\r\n\r\n{{callUrl}}
+share_button=Email
+copy_url_button=Copy
+copied_url_button=Copied!
+
+panel_footer_signin_or_signup_link=Sign In or Sign Up
+
+settings_menu_item_account=Account
+settings_menu_item_settings=Settings
+settings_menu_item_signout=Sign Out
+settings_menu_item_signin=Sign In
+settings_menu_button_tooltip=Settings
+
+# Contact Strings (Panel)
+
+## LOCALIZATION NOTE(contacts_search_placeholder): This is the placeholder text for
+## the search field at https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+contacts_search_placesholder=Search…
+
+## LOCALIZATION NOTE (new_contact_button): This is the button to open the
+## new contact sub-panel.
+## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+## for where this appears on the UI
+new_contact_button=New Contact
+## LOCALIZATION NOTE (new_contact_name_placeholder, new_contact_email_placeholder):
+## These are the placeholders for the fields for entering a new contact
+## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+## and click the 'New Contact' button to see the fields.
+new_contact_name_placeholder=Name
+new_contact_email_placeholder=Email
+## LOCALIZATION NOTE (add_contact_button):
+## This is the button to actually add the new contact
+## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+## and click the 'New Contact' button to see the fields.
+add_contact_button=Add Contact
+### LOCALIZATION NOTE (valid_email_text_description): This is displayed when
+### the user enters an invalid email address, preventing the addition of the
+### contact.
+valid_email_text_description=Please enter a valid email address
+
+## LOCALIZATION NOTE (add_or_import_contact_title): This is the subtitle of the panel
+## at https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+add_or_import_contact_title=Add or Import Contact
+## LOCALIZATION NOTE (import_contacts_button, importing_contacts_progress_button):
+## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+## for where these appear on the UI
+import_contacts_button=Import
+importing_contacts_progress_button=Importing…
+## LOCALIZATION NOTE(sync_contacts_button): This button is displayed in place of
+## importing_contacts_button once contacts have been imported once.
+sync_contacts_button=Sync Contacts
+## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
+## contacts fails. This is displayed in the error field here:
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
+import_failed_description_simple=Sorry, contact import failed
+## LOCALIZATION NOTE(import_failed_description): Displayed when an import of contacts
+## fails and the user may need more help. In this item, don't translate the part between
+## {{..}} because this will be replaced by the label from import_failed_support_link_label.
+## This is displayed in the error field here:
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
+import_failed_description=Sorry, contact import failed, please see our {{import_failed_support_link_label}} for help
+import_failed_support_link_label=support site
+
+## LOCALIZATION NOTE(remove_contact_menu_button): Displayed in the contact list in
+## a pop-up menu next to the contact's name.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+remove_contact_menu_button=Remove Contact
+## LOCALIZATION NOTE(confirm_delete_contact_alert): This is an alert that is displayed
+## to confirm deletion of a contact.
+confirm_delete_contact_alert=Are you sure you want to delete this contact?
+## LOCALIZATION NOTE(confirm_delete_contact_remove_button, confirm_delete_contact_cancel_button):
+## These are displayed on the alert with confirm_delete_contact_alert
+confirm_delete_contact_remove_button=Remove Contact
+confirm_delete_contact_cancel_button=Cancel
+
+## LOCALIZATION NOTE(block_contact_menu_button): Displayed in the contact list in
+## a pop-up menu next to the contact's name, used to block a contact from calling
+## the user. https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+block_contact_menu_button=Block Contact
+## LOCALIZATION NOTE(unblock_contact_menu_button): Displayed in the contact list in
+## a pop-up menu next to the contact's name, used to unblock a contact and allow them
+## to call the user. https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+unblock_contact_menu_button=Unblock Contact
+
+## LOCALIZATION NOTE(edit_contact_menu_button): Displayed in the contact list in a
+## pop-up menu next to the contact's name.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+edit_contact_menu_button=Edit Contact…
+## LOCALIZATION NOTE(edit_contact_tile): This is the subtitle of the edit contact
+## panel. It is displayed when Edit Contact is selected.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+edit_contact_title=Edit Contact
+## LOCALIZATION NOTE(edit_contact_name_label, edit_contact_email_label):
+## These fields are display when the Edit Contact button is selected.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+edit_contact_name_label=Name
+edit_contact_email_label=Email
+## LOCALIZATION NOTE(edit_contact_name_label, edit_contact_email_label):
+## These button is displayed when the Edit Contact button is selected and is used
+## to accept the change.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+edit_contact_done_button=Done
+
+## LOCALIZATION NOTE(audio_call_menu_button): Displayed in the contact list in a
+## pop-up menu next to the contact's name.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+audio_call_menu_button=Audio Conversation
+
+## LOCALIZATION NOTE(video_call_menu_button): Displayed in the contact list in a
+## pop-up menu next to the contact's name.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#contacts
+video_call_menu_button=Video Conversation
 
 # Conversation Window Strings
 
-incoming_call_title=Incoming Call…
-incoming_call=Incoming call
-incoming_call_answer_button=Answer
-incoming_call_answer_audio_only_tooltip=Answer with voice
-incoming_call_decline_button=Decline
-incoming_call_decline_and_block_button=Decline and Block
+initiate_call_button_label2=Ready to start your conversation?
+incoming_call_title2=Conversation Request
+incoming_call_accept_button=Accept
+incoming_call_accept_audio_only_tooltip=Accept with voice
+incoming_call_cancel_button=Cancel
+incoming_call_cancel_and_block_button=Cancel and Block
 incoming_call_block_button=Block
 hangup_button_title=Hang up
-hangup_button_caption=End Call
+hangup_button_caption2=Exit
 mute_local_audio_button_title=Mute your audio
 unmute_local_audio_button_title=Unmute your audio
 mute_local_video_button_title=Mute your video
 unmute_local_video_button_title=Unmute your video
 
+## LOCALIZATION NOTE (call_with_contact_title): The title displayed
+## when calling a contact. Don't translate the part between {{..}} because
+## this will be replaced by the contact's name.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
+call_with_contact_title=Conversation with {{contactName}}
+
+# Outgoing conversation
+
+outgoing_call_title=Start conversation?
+initiate_audio_video_call_button2=Start
+initiate_audio_video_call_tooltip2=Start a video conversation
+initiate_audio_call_button2=Voice conversation
+initiate_call_cancel_button=Cancel
+
+## LOCALIZATION NOTE (call_progress_connecting_description): This is displayed
+## whilst the client is contacting the client at the other end of the connection
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
+call_progress_connecting_description=Connecting…
+## LOCALIZATION NOTE (call_progress_ringing_description): This is displayed
+## when the other client is actually ringing.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
+call_progress_ringing_description=Ringing…
+
 peer_ended_conversation2=The person you were calling has ended the conversation.
-call_has_ended=Your call has ended.
+conversation_has_ended=Your conversation has ended.
+restart_call=Rejoin
+
+generic_failure_title=Something went wrong.
+generic_failure_with_reason2=You can try again or email a link to be reached at later.
+generic_failure_no_reason2=Would you like to try again?
+
+## LOCALIZATION NOTE (contact_offline_title): Title for
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
+## displayed when the contact is offline.
+contact_offline_title=This person is not online
+## LOCALIZATION NOTE (call_timeout_notification_text): Title for
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
+## displayed when the call didn't go through.
+call_timeout_notification_text=Your call did not go through.
+
+## LOCALIZATION NOTE (retry_call_button, cancel_button, email_link_button):
+## These buttons are displayed when a call has failed:
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
+retry_call_button=Retry
+email_link_button=Email Link
+cancel_button=Cancel
 
 cannot_start_call_session_not_ready=Can't start call, session is not ready.
 network_disconnected=The network connection terminated abruptly.
-
 connection_error_see_console_notification=Call failed; see console for details.
 
-## LOCALIZATION NOTE (legal_text_and_links2): In this item, don't translate the
+## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
 ## parts between {{..}} because these will be replaced with links with the labels
-## from legal_text_tos and legal_text_privacy.
-legal_text_and_links2=By using this product you agree to the {{terms_of_use}} \
+## from legal_text_tos and legal_text_privacy. clientShortname will be replaced
+## by the brand name, or fall back to client_shortname_fallback
+legal_text_and_links3=By using {{clientShortname}} you agree to the {{terms_of_use}} \
   and {{privacy_notice}}.
 legal_text_tos = Terms of Use
 legal_text_privacy = Privacy Notice
+client_shortname_fallback=this product
 
-feedback_call_experience_heading=How was your call experience?
+feedback_call_experience_heading2=How was your conversation?
 feedback_what_makes_you_sad=What makes you sad?
 feedback_thank_you_heading=Thank you for your feedback!
 feedback_category_audio_quality=Audio quality
 feedback_category_video_quality=Video quality
 feedback_category_was_disconnected=Was disconnected
 feedback_category_confusing=Confusing
 feedback_category_other=Other:
 feedback_custom_category_text_placeholder=What went wrong?
 feedback_submit_button=Submit
 feedback_back_button=Back
 ## LOCALIZATION NOTE (feedback_window_will_close_in2):
 ## Semicolon-separated list of plural forms. See:
 ## http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ## In this item, don't translate the part between {{..}}
 feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
-
-share_email_subject2=Invitation to chat
-## LOCALIZATION NOTE (share_email_body2): In this item, don't translate the
-## part between {{..}} and leave the \r\n\r\n part alone
-share_email_body2=Please click this link to call me:\r\n\r\n{{callUrl}}
-share_button=Email
-copy_url_button=Copy
-copied_url_button=Copied!
-
-panel_footer_signin_or_signup_link=Sign In or Sign Up
+## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
+## a signed-in to signed-in user call.
+## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
+feedback_rejoin_button=Rejoin
+## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
+## an abusive user.
+feedback_report_user_button=Report User
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1317,16 +1317,17 @@ toolbar .toolbarbutton-1 > .toolbarbutto
     list-style-image: url("chrome://browser/skin/loop/toolbar@2x.png");
     -moz-image-region: rect(0, 36px, 36px, 0);
   }
 
   toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
     list-style-image: url("chrome://browser/skin/loop/toolbar-inverted@2x.png");
   }
 
+  #loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
   #loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 72px, 36px, 36px);
   }
 
   #loop-call-button:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 108px, 36px, 72px);
   }
 
@@ -1352,16 +1353,17 @@ toolbar .toolbarbutton-1 > .toolbarbutto
     -moz-image-region: rect(0, 64px, 64px, 0);
   }
 
   /* Make sure that the state icons are not shown in the customization palette. */
   toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 64px, 64px, 0) !important;
   }
 
+  #loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
   #loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 128px, 64px, 64px);
   }
 
   #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 192px, 64px, 128px);
   }
 
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -156,16 +156,17 @@ toolbarpaletteitem[place="palette"] > #l
   -moz-image-region: rect(0, 32px, 32px, 0);
 }
 
 /* Make sure that the state icons are not shown in the customization palette. */
 toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 32px, 32px, 0) !important;
 }
 
+#loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
 #loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 64px, 32px, 32px);
 }
 
 #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 96px, 32px, 64px);
 }
 
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -172,16 +172,17 @@ toolbar[brighttext] #sync-button[status=
   list-style-image: url(chrome://browser/skin/loop/toolbar.png);
   -moz-image-region: rect(0, 18px, 18px, 0);
 }
 
 toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
   list-style-image: url(chrome://browser/skin/loop/toolbar-inverted.png);
 }
 
+#loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
 #loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 36px, 18px, 18px);
 }
 
 #loop-call-button:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 54px, 18px, 36px);
 }
 
--- a/configure.in
+++ b/configure.in
@@ -3828,16 +3828,17 @@ USE_ARM_KUSER=
 BUILD_CTYPES=1
 MOZ_USE_NATIVE_POPUP_WINDOWS=
 MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
 MOZ_LOCALE_SWITCHER=
 MOZ_ANDROID_SEARCH_ACTIVITY=
 MOZ_ANDROID_MLS_STUMBLER=
+MOZ_ANDROID_SHARE_OVERLAY=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
 MOZ_PAY=
 MOZ_AUDIO_CHANNEL_MANAGER=
 NSS_NO_LIBPKIX=
 MOZ_CONTENT_SANDBOX=
 MOZ_GMP_SANDBOX=
 MOZ_SANDBOX=1
@@ -4851,16 +4852,23 @@ fi
 dnl ========================================================
 dnl = Include Mozilla Location Service Stumbler on Android
 dnl ========================================================
 if test -n "$MOZ_ANDROID_MLS_STUMBLER"; then
     AC_DEFINE(MOZ_ANDROID_MLS_STUMBLER)
 fi
 
 dnl ========================================================
+dnl = Include share overlay on Android
+dnl ========================================================
+if test -n "$MOZ_ANDROID_SHARE_OVERLAY"; then
+    AC_DEFINE(MOZ_ANDROID_SHARE_OVERLAY)
+fi
+
+dnl ========================================================
 dnl = Enable IPDL's "expensive" unit tests
 dnl ========================================================
 MOZ_IPDL_TESTS=
 
 MOZ_ARG_ENABLE_BOOL(ipdl-tests,
 [  --enable-ipdl-tests     Enable expensive IPDL tests],
     MOZ_IPDL_TESTS=1,
     MOZ_IPDL_TESTS=)
@@ -8442,16 +8450,17 @@ AC_SUBST(MOZ_D3DCOMPILER_XP_CAB)
 AC_SUBST(MOZ_METRO)
 
 AC_SUBST(MOZ_ANDROID_HISTORY)
 AC_SUBST(MOZ_WEBSMS_BACKEND)
 AC_SUBST(MOZ_ANDROID_BEAM)
 AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
 AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
+AC_SUBST(MOZ_ANDROID_SHARE_OVERLAY)
 AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
 AC_SUBST(ENABLE_STRIP)
 AC_SUBST(PKG_SKIP_STRIP)
 AC_SUBST(STRIP_FLAGS)
 AC_SUBST(USE_ELF_HACK)
 AC_SUBST(INCREMENTAL_LINKER)
 AC_SUBST(MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS)
 AC_SUBST(MOZ_COMPONENT_NSPR_LIBS)
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -366,16 +366,35 @@
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER">
 
             <path-permission android:pathPrefix="/search_suggest_query"
                              android:readPermission="android.permission.GLOBAL_SEARCH" />
 
         </provider>
 
+#ifdef MOZ_ANDROID_SHARE_OVERLAY
+        <!-- Share overlay activity -->
+        <activity android:name="org.mozilla.gecko.overlays.ui.ShareDialog"
+                  android:label="@string/overlay_share_header"
+                  android:theme="@style/ShareOverlayActivity"
+                  android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
+                  android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
+
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="text/plain" />
+            </intent-filter>
+
+        </activity>
+
+        <!-- Service to handle requests from overlays. -->
+        <service android:name="org.mozilla.gecko.overlays.service.OverlayActionService" />
+#endif
         <!--
           Ensure that passwords provider runs in its own process. (Bug 718760.)
           Process name is per-application to avoid loading CPs from multiple
           Fennec versions into the same process. (Bug 749727.)
           Process name is a mangled version to avoid a Talos bug. (Bug 750548.)
           -->
         <provider android:name="org.mozilla.gecko.db.PasswordsProvider"
                   android:label="@string/sync_configure_engines_title_passwords"
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -85,16 +85,33 @@
 <!ENTITY pref_category_language "Language">
 <!ENTITY pref_browser_locale "Browser language">
 
 <!-- Localization note (locale_system_default) : This string indicates that
      Firefox will use the locale currently selected in Android's settings
      to display browser chrome. -->
 <!ENTITY locale_system_default "System default">
 
+<!-- Localization note (overlay_share_bookmark_btn_label) : This string is
+     used in the share overlay menu to select an action. It is the verb
+     "to bookmark", not the noun "a bookmark". -->
+<!ENTITY overlay_share_bookmark_btn_label "Bookmark">
+<!ENTITY overlay_share_reading_list_btn_label "Add to Reading List">
+<!ENTITY overlay_share_header "Send to &brandShortName;">
+<!ENTITY overlay_share_send_other "Send to other devices">
+
+<!-- Localization note (overlay_share_send_tab_btn_label) : Used on the
+     share overlay menu to represent the "Send Tab" action when the user
+     either has not set up Sync, or has no other devices to send a tab
+     to. -->
+<!ENTITY overlay_share_send_tab_btn_label "Send to another device">
+<!ENTITY overlay_share_no_url "No link found in this share">
+<!ENTITY overlay_share_retry "Try again">
+<!ENTITY overlay_share_select_device "Select device">
+
 <!ENTITY pref_category_search3 "Search">
 <!ENTITY pref_category_search_summary "Customize your search providers">
 <!ENTITY pref_category_display "Display">
 <!ENTITY pref_category_privacy_short "Privacy">
 <!ENTITY pref_category_vendor "&vendorShortName;">
 <!ENTITY pref_category_datareporting "Data choices">
 <!ENTITY pref_learn_more "Learn more">
 <!ENTITY pref_category_installed_search_engines "Installed search engines">
--- a/mobile/android/base/locales/moz.build
+++ b/mobile/android/base/locales/moz.build
@@ -1,8 +1,11 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
     DEFINES['MOZ_ANDROID_SEARCH_ACTIVITY'] = 1
+
+if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
+    DEFINES['MOZ_ANDROID_SHARE_OVERLAY'] = 1
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -474,16 +474,32 @@ gbjar.generated_sources += [
     'org/mozilla/gecko/widget/ThemedTextSwitcher.java',
     'org/mozilla/gecko/widget/ThemedTextView.java',
     'org/mozilla/gecko/widget/ThemedView.java',
 ]
 if CONFIG['MOZ_CRASHREPORTER']:
     gbjar.sources += [ 'CrashReporter.java' ]
     ANDROID_RES_DIRS += [ SRCDIR + '/crashreporter/res' ]
 
+if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
+    gbjar.sources += [
+        'overlays/OverlayConstants.java',
+        'overlays/service/OverlayActionService.java',
+        'overlays/service/sharemethods/AddBookmark.java',
+        'overlays/service/sharemethods/AddToReadingList.java',
+        'overlays/service/sharemethods/ParcelableClientRecord.java',
+        'overlays/service/sharemethods/SendTab.java',
+        'overlays/service/sharemethods/ShareMethod.java',
+        'overlays/ui/OverlayToastHelper.java',
+        'overlays/ui/SendTabDeviceListArrayAdapter.java',
+        'overlays/ui/SendTabList.java',
+        'overlays/ui/SendTabTargetSelectedListener.java',
+        'overlays/ui/ShareDialog.java',
+    ]
+
 gbjar.sources += sync_java_files
 gbjar.generated_sources += sync_generated_java_files
 gbjar.extra_jars = [
     'gecko-R.jar',
     'gecko-mozglue.jar',
     'gecko-util.jar',
     'nineoldandroids.jar',
     'squareup-picasso.jar',
@@ -582,17 +598,18 @@ ANDROID_RES_DIRS += [
 ]
 
 ANDROID_GENERATED_RESFILES += [
     'res/raw/suggestedsites.json',
     'res/values/strings.xml',
 ]
 
 for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
-            'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER'):
+            'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
+            'MOZ_ANDROID_SHARE_OVERLAY'):
     if CONFIG[var]:
         DEFINES[var] = 1
 
 for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL'):
     if CONFIG[var]:
         DEFINES[var] = CONFIG[var]
 
 for var in ('ANDROID_PACKAGE_NAME', 'ANDROID_CPU_ARCH', 'CPU_ARCH',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/OverlayConstants.java
@@ -0,0 +1,68 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays;
+
+/**
+ * Constants used by the share handler service (and clients).
+ * The intent API used by the service is defined herein.
+ */
+public class OverlayConstants {
+    /*
+     * OverlayIntentHandler service intent actions.
+     */
+
+    /*
+     * Causes the service to broadcast an intent containing state necessary for proper display of
+     * a UI to select a target share method.
+     *
+     * Intent parameters:
+     *
+     * None.
+     */
+    public static final String ACTION_PREPARE_SHARE = "org.mozilla.gecko.overlays.ACTION_PREPARE_SHARE";
+
+    /*
+     * Action for sharing a page.
+     *
+     * Intent parameters:
+     *
+     * $EXTRA_URL: URL of page to share.    (required)
+     * $EXTRA_SHARE_METHOD: Method(s) via which to share this url/title combination. Can be either a
+     *                ShareType or a ShareType[]
+     * $EXTRA_TITLE: Title of page to share (optional)
+     * $EXTRA_PARAMETERS: Parcelable of extra data to pass to the ShareMethod (optional)
+     */
+    public static final String ACTION_SHARE = "org.mozilla.gecko.overlays.ACTION_SHARE";
+
+    /*
+     * OverlayIntentHandler service intent extra field keys.
+     */
+
+    // The URL/title of the page being shared
+    public static final String EXTRA_URL = "URL";
+    public static final String EXTRA_TITLE = "TITLE";
+
+    // The optional extra Parcelable parameters for a ShareMethod.
+    public static final String EXTRA_PARAMETERS = "EXTRA";
+
+    // The extra field key used for holding the ShareMethod.Type we wish to use for an operation.
+    public static final String EXTRA_SHARE_METHOD = "SHARE_METHOD";
+
+    /*
+     * ShareMethod UI event intent constants. Broadcast by ShareMethods using LocalBroadcastManager
+     * when state has changed that requires an update of any currently-displayed share UI.
+     */
+
+    /*
+     * Action for a ShareMethod UI event.
+     *
+     * Intent parameters:
+     *
+     * $EXTRA_SHARE_METHOD: The ShareType to which this event relates.
+     * ... ShareType-specific parameters as desired... (optional)
+     */
+    public static final String SHARE_METHOD_UI_EVENT = "org.mozilla.gecko.overlays.ACTION_SHARE_METHOD_UI_EVENT";
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/service/OverlayActionService.java
@@ -0,0 +1,148 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.service;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.View;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.overlays.OverlayConstants;
+import org.mozilla.gecko.overlays.service.sharemethods.AddBookmark;
+import org.mozilla.gecko.overlays.service.sharemethods.AddToReadingList;
+import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
+import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
+import org.mozilla.gecko.overlays.ui.OverlayToastHelper;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_PREPARE_SHARE;
+import static org.mozilla.gecko.overlays.OverlayConstants.ACTION_SHARE;
+import static org.mozilla.gecko.overlays.OverlayConstants.EXTRA_SHARE_METHOD;
+
+/**
+ * A service to receive requests from overlays to perform actions.
+ * See OverlayConstants for details of the intent API supported by this service.
+ *
+ * Currently supported operations are:
+ *
+ * Add bookmark*
+ * Add to reading list*
+ * Send tab (delegates to Sync's existing handler)
+ * Future: Load page in background.
+ *
+ * * Neither of these incur a page fetch on the service... yet. That will require headless Gecko,
+ *   something we're yet to have. Refactoring Gecko as a service itself and restructing the rest of
+ *   the app to talk to it seems like the way to go there.
+ */
+public class OverlayActionService extends Service {
+    private static final String LOGTAG = "GeckoOverlayService";
+
+    // Map used for selecting the appropriate helper object when handling a share.
+    private final Map<ShareMethod.Type, ShareMethod> shareTypes = new EnumMap<>(ShareMethod.Type.class);
+
+    // Map relating Strings representing share types to the corresponding ShareMethods.
+    // Share methods are initialised (and shown in the UI) in the order they are given here.
+    // This map is used to look up the appropriate ShareMethod when handling a request, as well as
+    // for identifying which ShareMethod needs re-initialising in response to such an intent (which
+    // will be necessary in situations such as the deletion of Sync accounts).
+
+    // Not a bindable service.
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent == null) {
+            return START_NOT_STICKY;
+        }
+
+        // Dispatch intent to appropriate method according to its action.
+        String action = intent.getAction();
+
+        switch (action) {
+            case ACTION_SHARE:
+                handleShare(intent);
+                break;
+            case ACTION_PREPARE_SHARE:
+                initShareMethods(getApplicationContext());
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported intent action: " + action);
+        }
+
+        return START_NOT_STICKY;
+    }
+
+    /**
+     * Reinitialise all ShareMethods, causing them to broadcast any UI update events necessary.
+     */
+    private void initShareMethods(Context context) {
+        shareTypes.clear();
+
+        shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context));
+        shareTypes.put(ShareMethod.Type.ADD_TO_READING_LIST, new AddToReadingList(context));
+        shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context));
+    }
+
+    public void handleShare(final Intent intent) {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                Bundle extras = intent.getExtras();
+
+                // Fish the parameters out of the Intent.
+                final String url = extras.getString(OverlayConstants.EXTRA_URL);
+                final String title = extras.getString(OverlayConstants.EXTRA_TITLE);
+                final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS);
+
+                if (url == null) {
+                    Log.e(LOGTAG, "Null url passed to handleShare!");
+                    return;
+                }
+
+                ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD);
+                ShareMethod shareMethod = shareTypes.get(shareMethodType);
+
+                final ShareMethod.Result result = shareMethod.handle(title, url, extra);
+                // Dispatch the share to the targeted ShareMethod.
+                switch (result) {
+                    case SUCCESS:
+                        // \o/
+                        OverlayToastHelper.showSuccessToast(getApplicationContext(), shareMethod.getSuccessMesssage());
+                        break;
+                    case TRANSIENT_FAILURE:
+                        // An OnClickListener to do this share again.
+                        View.OnClickListener retryListener = new View.OnClickListener() {
+                            @Override
+                            public void onClick(View view) {
+                                handleShare(intent);
+                            }
+                        };
+
+                        // Show a failure toast with a retry button.
+                        OverlayToastHelper.showFailureToast(getApplicationContext(), shareMethod.getFailureMessage(), retryListener);
+                        break;
+                    case PERMANENT_FAILURE:
+                        // Show a failure toast without a retry button.
+                        OverlayToastHelper.showFailureToast(getApplicationContext(), shareMethod.getFailureMessage());
+                        break;
+                    default:
+                        Assert.isTrue(false, "Unknown share method result code: " + result);
+                        break;
+                }
+            }
+        });
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/service/sharemethods/AddBookmark.java
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.service.sharemethods;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Parcelable;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.LocalBrowserDB;
+
+public class AddBookmark extends ShareMethod {
+    private static final String LOGTAG = "GeckoAddBookmark";
+
+    @Override
+    public Result handle(String title, String url, Parcelable unused) {
+        ContentResolver resolver = context.getContentResolver();
+
+        LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
+        browserDB.addBookmark(resolver, url, title);
+
+        return Result.SUCCESS;
+    }
+
+    public String getSuccessMesssage() {
+        return context.getResources().getString(R.string.bookmark_added);
+    }
+
+    // Unused.
+    @Override
+    public String getFailureMessage() {
+        return null;
+    }
+
+    public AddBookmark(Context context) {
+        super(context);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/service/sharemethods/AddToReadingList.java
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.service.sharemethods;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.Parcelable;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.LocalBrowserDB;
+
+import static org.mozilla.gecko.db.BrowserContract.Bookmarks;
+
+/**
+ * ShareMethod to add a page to the reading list.
+ *
+ * Inserts the given URL/title pair into the reading list database.
+ * TODO: In the event the page turns out not to be reader-mode-compatible, freezes sometimes occur
+ * when we subsequently load the page in reader mode. (Bug 1044781)
+ */
+public class AddToReadingList extends ShareMethod {
+    private static final String LOGTAG = "GeckoAddToReadingList";
+
+    @Override
+    public Result handle(String title, String url, Parcelable unused) {
+        ContentResolver resolver = context.getContentResolver();
+
+        LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
+
+        ContentValues values = new ContentValues();
+        values.put(Bookmarks.TITLE, title);
+        values.put(Bookmarks.URL, url);
+
+        browserDB.addReadingListItem(resolver, values);
+
+        return Result.SUCCESS;
+    }
+
+    @Override
+    public String getSuccessMesssage() {
+        return context.getResources().getString(R.string.reading_list_added);
+    }
+
+    // Unused.
+    @Override
+    public String getFailureMessage() {
+        return null;
+    }
+
+    public AddToReadingList(Context context) {
+        super(context);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/service/sharemethods/ParcelableClientRecord.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.service.sharemethods;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+
+/**
+ * An immutable representation of a Sync ClientRecord for Parceling, storing only name, guid, type.
+ * Implemented this way instead of by making ClientRecord itself parcelable to avoid an undesirable
+ * dependency between Sync and the IPC system used by the share system (things which really should
+ * be kept as independent as possible).
+ */
+public class ParcelableClientRecord implements Parcelable {
+    private static final String LOGTAG = "GeckoParcelableClientRecord";
+
+    public final String name;
+    public final String type;
+    public final String guid;
+
+    private ParcelableClientRecord(String aName, String aType, String aGUID) {
+        name = aName;
+        type = aType;
+        guid = aGUID;
+    }
+
+    /**
+     * Create a ParcelableClientRecord from a vanilla ClientRecord.
+     */
+    public static ParcelableClientRecord fromClientRecord(ClientRecord record) {
+        return new ParcelableClientRecord(record.name, record.type, record.guid);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeString(name);
+        parcel.writeString(type);
+        parcel.writeString(guid);
+    }
+
+    public static final Creator<ParcelableClientRecord> CREATOR = new Creator<ParcelableClientRecord>() {
+        @Override
+        public ParcelableClientRecord createFromParcel(final Parcel source) {
+            String name = source.readString();
+            String type = source.readString();
+            String guid = source.readString();
+
+            return new ParcelableClientRecord(name, type, guid);
+        }
+
+        @Override
+        public ParcelableClientRecord[] newArray(final int size) {
+            return new ParcelableClientRecord[size];
+        }
+    };
+
+    /**
+     * Used by SendTabDeviceListArrayAdapter to populate ListViews.
+     */
+    @Override
+    public String toString() {
+        return name;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/service/sharemethods/SendTab.java
@@ -0,0 +1,370 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.service.sharemethods;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
+import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.overlays.OverlayConstants;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.CommandRunner;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConstants;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ShareMethod implementation to handle Sync's "Send tab to device" mechanism.
+ * See OverlayConstants for documentation of OverlayIntentHandler service intent API (which is how
+ * this class is chiefly interacted with).
+ */
+public class SendTab extends ShareMethod {
+    private static final String LOGTAG = "GeckoSendTab";
+
+    // Key used in the extras Bundle in the share intent used for a send tab ShareMethod.
+    public static final String SEND_TAB_TARGET_DEVICES = "SEND_TAB_TARGET_DEVICES";
+
+    // Key used in broadcast intent from SendTab ShareMethod specifying available ClientRecords.
+    public static final String EXTRA_CLIENT_RECORDS = "RECORDS";
+
+    // The intent we should dispatch when the button for this ShareMethod is tapped, instead of
+    // taking the normal action (eg. "Set up sync!")
+    public static final String OVERRIDE_INTENT = "OVERRIDE_INTENT";
+
+    private Set<String> validGUIDs;
+
+    // A TabSender appropriate to the account type we're connected to.
+    private TabSender tabSender;
+
+    @Override
+    public Result handle(String title, String url, Parcelable extra) {
+        if (extra == null) {
+            Log.e(LOGTAG, "No target devices specified!");
+
+            // Retrying with an identical lack of devices ain't gonna fix it...
+            return Result.PERMANENT_FAILURE;
+        }
+
+        String[] targetGUIDs = ((Bundle) extra).getStringArray(SEND_TAB_TARGET_DEVICES);
+
+        // Ensure all target GUIDs are devices we actually know about.
+        if (!validGUIDs.containsAll(Arrays.asList(targetGUIDs))) {
+            // Find the set of invalid GUIDs to provide a nice error message.
+            Log.e(LOGTAG, "Not all provided GUIDs are real devices:");
+            for (String targetGUID : targetGUIDs) {
+                if (!validGUIDs.contains(targetGUID)) {
+                    Log.e(LOGTAG, "Invalid GUID: " + targetGUID);
+                }
+            }
+
+            return Result.PERMANENT_FAILURE;
+        }
+
+        Log.i(LOGTAG, "Send tab handler invoked.");
+
+        final CommandProcessor processor = CommandProcessor.getProcessor();
+
+        final String accountGUID = tabSender.getAccountGUID();
+        Log.d(LOGTAG, "Retrieved local account GUID '" + accountGUID + "'.");
+
+        if (accountGUID == null) {
+            Log.e(LOGTAG, "Cannot determine account GUID");
+
+            // It's not completely out of the question that a background sync might come along and
+            // fix everything for us...
+            return Result.TRANSIENT_FAILURE;
+        }
+
+        // Queue up the share commands for each destination device.
+        // Remember that ShareMethod.handle is always run on the background thread, so the database
+        // access here is of no concern.
+        for (int i = 0; i < targetGUIDs.length; i++) {
+            processor.sendURIToClientForDisplay(url, targetGUIDs[i], title, accountGUID, context);
+        }
+
+        // Request an immediate sync to push these new commands to the network ASAP.
+        Log.i(LOGTAG, "Requesting immediate clients stage sync.");
+        tabSender.sync();
+
+        return Result.SUCCESS;
+        // ... Probably.
+    }
+
+    /**
+     * Get an Intent suitable for broadcasting the UI state of this ShareMethod.
+     * The caller shall populate the intent with the actual state.
+     */
+    private Intent getUIStateIntent() {
+        Intent uiStateIntent = new Intent(OverlayConstants.SHARE_METHOD_UI_EVENT);
+        uiStateIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) Type.SEND_TAB);
+        return uiStateIntent;
+    }
+
+    /**
+     * Broadcast the given intent to any UIs that may be listening.
+     */
+    private void broadcastUIState(Intent uiStateIntent) {
+        LocalBroadcastManager.getInstance(context).sendBroadcast(uiStateIntent);
+    }
+
+    /**
+     * Load the state of the user's Firefox Sync accounts and broadcast it to any registered
+     * listeners. This will cause any UIs that may exist that depend on this information to update.
+     */
+    public SendTab(Context aContext) {
+        super(aContext);
+        // Initialise the UI state intent...
+
+        // Determine if the user has a new or old style sync account and load the available sync
+        // clients for it.
+        final AccountManager accountManager = AccountManager.get(context);
+        final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
+
+        if (fxAccounts.length > 0) {
+            final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]);
+            if (fxAccount.getState().getNeededAction() != State.Action.None) {
+                // We have a Firefox Account, but it's definitely not able to send a tab
+                // right now. Redirect to the status activity.
+                Log.w(LOGTAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
+                              " needs action before it can send a tab; redirecting to status activity.");
+
+                setOverrideIntent(FxAccountStatusActivity.class);
+                return;
+            }
+
+            tabSender = new FxAccountTabSender(fxAccount);
+
+            updateClientList(tabSender);
+
+            Log.i(LOGTAG, "Allowing tab send for Firefox Account.");
+            registerDisplayURICommand();
+            return;
+        }
+
+        final Account[] syncAccounts = accountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC);
+        if (syncAccounts.length > 0) {
+            tabSender = new Sync11TabSender(context, syncAccounts[0], accountManager);
+
+            updateClientList(tabSender);
+
+            Log.i(LOGTAG, "Allowing tab send for Sync account.");
+            registerDisplayURICommand();
+            return;
+        }
+
+        // Have registered UIs offer to set up a Firefox Account.
+        setOverrideIntent(FxAccountGetStartedActivity.class);
+    }
+
+    /**
+     * Load the list of Sync clients that are not this device using the given TabSender.
+     */
+    private void updateClientList(TabSender tabSender) {
+        Collection<ClientRecord> otherClients = getOtherClients(tabSender);
+
+        ParcelableClientRecord[] records = new ParcelableClientRecord[otherClients.size()];
+        validGUIDs = new HashSet<>();
+        int i = 0;
+
+        // Put the list of ClientRecords into the uiStateIntent and broadcast it.
+        for (ClientRecord client : otherClients) {
+            ParcelableClientRecord record = ParcelableClientRecord.fromClientRecord(client);
+
+            records[i] = record;
+
+            validGUIDs.add(record.guid);
+            i++;
+        }
+
+        Intent uiStateIntent = getUIStateIntent();
+        uiStateIntent.putExtra(EXTRA_CLIENT_RECORDS, records);
+        broadcastUIState(uiStateIntent);
+    }
+
+    /**
+     * Record our intention to redirect the user to a different activity when they attempt to share
+     * with us, usually because we found something wrong with their Sync account (a need to login,
+     * register, etc.)
+     * This will be recorded in the OVERRIDE_INTENT field of the UI broadcast. Consumers should
+     * dispatch this intent instead of attempting to share with this ShareMethod whenever it is
+     * non-null.
+     *
+     * @param activityClass The class of the activity we wish to launch instead of invoking a share.
+     */
+    protected void setOverrideIntent(Class<? extends Activity> activityClass) {
+        Intent intent = new Intent(context, activityClass);
+        // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+        // the soft keyboard not being shown for the started activity. Why, Android, why?
+        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        Intent uiStateIntent = getUIStateIntent();
+        uiStateIntent.putExtra(OVERRIDE_INTENT, intent);
+        broadcastUIState(uiStateIntent);
+    }
+
+    private static void registerDisplayURICommand() {
+        final CommandProcessor processor = CommandProcessor.getProcessor();
+        processor.registerCommand("displayURI", new CommandRunner(3) {
+            @Override
+            public void executeCommand(final GlobalSession session, List<String> args) {
+                CommandProcessor.displayURI(args, session.getContext());
+            }
+        });
+    }
+
+    /**
+     * @return A map from GUID to client record for all sync clients, including our own; or null iff
+     * ClientsDatabaseAccessor.fetchAllClients throws NullCursorException.
+     */
+    protected Map<String, ClientRecord> getAllClients() {
+        ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
+        try {
+            return db.fetchAllClients();
+        } catch (NullCursorException e) {
+            Log.w(LOGTAG, "NullCursorException while populating device list.", e);
+            return null;
+        } finally {
+            db.close();
+        }
+    }
+
+    /**
+     * @return a collection of client records, excluding our own.
+     */
+    protected Collection<ClientRecord> getOtherClients(final TabSender sender) {
+        if (sender == null) {
+            Log.w(LOGTAG, "No tab sender when fetching other client IDs.");
+            return Collections.emptyList();
+        }
+
+        final Map<String, ClientRecord> all = getAllClients();
+        if (all == null) {
+            return Collections.emptyList();
+        }
+
+        final String ourGUID = sender.getAccountGUID();
+        if (ourGUID == null) {
+            return all.values();
+        }
+
+        final ArrayList<ClientRecord> out = new ArrayList<>(all.size());
+        for (Map.Entry<String, ClientRecord> entry : all.entrySet()) {
+            if (!ourGUID.equals(entry.getKey())) {
+                out.add(entry.getValue());
+            }
+        }
+
+        return out;
+    }
+
+    @Override
+    public String getSuccessMesssage() {
+        return context.getResources().getString(R.string.sync_text_tab_sent);
+    }
+
+    @Override
+    public String getFailureMessage() {
+        return context.getResources().getString(R.string.sync_text_tab_not_sent);
+    }
+
+    /**
+     * Inteface for interacting with Sync accounts. Used to hide the difference in implementation
+     * between FXA and "old sync" accounts when sending tabs.
+     */
+    private interface TabSender {
+        public static final String[] STAGES_TO_SYNC = new String[] { "clients", "tabs" };
+
+        /**
+         * @return Return null if the account isn't correctly initialized. Return
+         *         the account GUID otherwise.
+         */
+        String getAccountGUID();
+
+        /**
+         * Sync this account, specifying only clients and tabs as the engines to sync.
+         */
+        void sync();
+    }
+
+    private static class FxAccountTabSender implements TabSender {
+        private final AndroidFxAccount fxAccount;
+
+        public FxAccountTabSender(AndroidFxAccount fxa) {
+            fxAccount = fxa;
+        }
+
+        @Override
+        public String getAccountGUID() {
+            try {
+                final SharedPreferences prefs = fxAccount.getSyncPrefs();
+                return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Could not get Firefox Account parameters or preferences; aborting.");
+                return null;
+            }
+        }
+
+        @Override
+        public void sync() {
+            fxAccount.requestSync(FirefoxAccounts.FORCE, STAGES_TO_SYNC, null);
+        }
+    }
+
+    private static class Sync11TabSender implements TabSender {
+        private final Account account;
+        private final AccountManager accountManager;
+        private final Context context;
+
+        private Sync11TabSender(Context aContext, Account syncAccount, AccountManager manager) {
+            context = aContext;
+            account = syncAccount;
+            accountManager = manager;
+        }
+
+        @Override
+        public String getAccountGUID() {
+            try {
+                SharedPreferences prefs = SyncAccounts.blockingPrefsFromDefaultProfileV0(context, accountManager, account);
+                return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Could not get Sync account parameters or preferences; aborting.");
+                return null;
+            }
+        }
+
+        @Override
+        public void sync() {
+            SyncAdapter.requestImmediateSync(account, STAGES_TO_SYNC);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/service/sharemethods/ShareMethod.java
@@ -0,0 +1,92 @@
+/*This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.service.sharemethods;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a method of sharing a URL/title. Add a bookmark? Send to a device? Add to reading list?
+ */
+public abstract class ShareMethod {
+    protected final Context context;
+
+    public ShareMethod(Context aContext) {
+        context = aContext;
+    }
+
+    /**
+     * Perform a share for the given title/URL combination. Called on the background thread by the
+     * handler service when a request is made. The "extra" parameter is provided should a ShareMethod
+     * desire to handle the share differently based on some additional parameters.
+     *
+     * @param title The page title for the page being shared. May be null if none can be found.
+     * @param url The URL of the page to be shared. Never null.
+     * @param extra A Parcelable of ShareMethod-specific parameters that may be provided by the
+     *              caller. Generally null, but this field may be used to provide extra input to
+     *              the ShareMethod (such as the device to share to in the case of SendTab).
+     * @return true if the attempt to share was a success. False in the event of an error.
+     */
+    public abstract Result handle(String title, String url, Parcelable extra);
+
+    /**
+     * Convenience method for calling handlers on objects that don't require extra data.
+     */
+    public Result handle(String title, String url) {
+        return handle(title, url, null);
+    }
+
+    public abstract String getSuccessMesssage();
+    public abstract String getFailureMessage();
+
+    /**
+     * Enum representing the possible results of performing a share.
+     */
+    public static enum Result {
+        // Victory!
+        SUCCESS,
+
+        // Failure, but retrying the same action again might lead to success.
+        TRANSIENT_FAILURE,
+
+        // Failure, and you're not going to succeed until you reinitialise the ShareMethod (ie.
+        // until you repeat the entire share action). Examples include broken Sync accounts, or
+        // Sync accounts with no valid target devices (so the only way to fix this is to add some
+        // and try again: pushing a retry button isn't sane).
+        PERMANENT_FAILURE
+    }
+
+    /**
+     * Enum representing types of ShareMethod. Parcelable so it may be efficiently used in Intents.
+     */
+    public static enum Type implements Parcelable {
+        ADD_BOOKMARK,
+        ADD_TO_READING_LIST,
+        SEND_TAB;
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(final Parcel dest, final int flags) {
+            dest.writeInt(ordinal());
+        }
+
+        public static final Creator<Type> CREATOR = new Creator<Type>() {
+            @Override
+            public Type createFromParcel(final Parcel source) {
+                return Type.values()[source.readInt()];
+            }
+
+            @Override
+            public Type[] newArray(final int size) {
+                return new Type[size];
+            }
+        };
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/ui/OverlayToastHelper.java
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.ui;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.mozilla.gecko.R;
+
+/**
+ * Static helper class for generating toasts for share events.
+ *
+ * The overlay toasts come in a variety of flavours: success (rectangle with happy green tick,
+ * failure (no tick, a retry button), and success-with-tutorial (as success, but with a pretty
+ * picture of some description to educate the user on how to use the feature) TODO: Bug 1048645.
+ */
+public class OverlayToastHelper {
+    /**
+     * Show a toast indicating a failure to share.
+     * @param context Context in which to inflate the toast.
+     * @param failureMessage String to display in the toast.
+     * @param isTransient Should a retry button be presented?
+     * @param retryListener Listener to fire when the retry button is pressed.
+     */
+    public static void showFailureToast(Context context, String failureMessage, View.OnClickListener retryListener) {
+        showToast(context, failureMessage, false, retryListener);
+    }
+    public static void showFailureToast(Context context, String failureMessage) {
+        showFailureToast(context, failureMessage, null);
+    }
+
+    /**
+     * Show a toast indicating a successful share.
+     * @param successMessage Message to show in the toast.
+     */
+    public static void showSuccessToast(Context context, String successMessage) {
+        showToast(context, successMessage, true, null);
+    }
+
+    private static void showToast(Context context, String message, boolean success, View.OnClickListener retryListener) {
+        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        View layout = inflater.inflate(R.layout.overlay_share_toast, null);
+
+        TextView text = (TextView) layout.findViewById(R.id.overlay_toast_message);
+        text.setText(message);
+
+        if (retryListener == null) {
+            // Hide the retry button.
+            layout.findViewById(R.id.overlay_toast_separator).setVisibility(View.GONE);
+            layout.findViewById(R.id.overlay_toast_retry_btn).setVisibility(View.GONE);
+        } else {
+            // Set up the button to perform a retry.
+            Button retryBtn = (Button) layout.findViewById(R.id.overlay_toast_retry_btn);
+            retryBtn.setOnClickListener(retryListener);
+        }
+
+        if (!success) {
+            // Hide the happy green tick.
+            text.setCompoundDrawables(null, null, null, null);
+        }
+
+        Toast toast = new Toast(context);
+        toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.BOTTOM, 0, 0);
+        toast.setDuration(Toast.LENGTH_SHORT);
+        toast.setView(layout);
+        toast.show();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/ui/SendTabDeviceListArrayAdapter.java
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
+
+import java.util.Collection;
+
+import static org.mozilla.gecko.overlays.ui.SendTabList.*;
+
+public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClientRecord> {
+    private static final String LOGTAG = "GeckoSendTabAdapter";
+
+    private State currentState;
+
+    // String to display when in a "button-like" special state. Instead of using a
+    // ParcelableClientRecord we override the rendering using this string.
+    private String dummyRecordName;
+
+    private final SendTabTargetSelectedListener listener;
+
+    private Collection<ParcelableClientRecord> records;
+
+    // The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state.
+    // This will show the user a prompt to select a device from a longer list of devices.
+    private AlertDialog dialog;
+
+    public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener, int textViewResourceId) {
+        super(context, textViewResourceId);
+
+        listener = aListener;
+
+        // We do this manually and avoid multiple notifications when doing compound operations.
+        setNotifyOnChange(false);
+    }
+
+    /**
+     * Get an array of the contents of this adapter were it in the LIST state.
+     * Useful for determining the "real" contents of the adapter.
+     */
+    public ParcelableClientRecord[] toArray() {
+        return records.toArray(new ParcelableClientRecord[records.size()]);
+    }
+
+    public void setClientRecordList(Collection<ParcelableClientRecord> clientRecordList) {
+        records = clientRecordList;
+        updateRecordList();
+    }
+
+    /**
+     * Ensure the contents of the Adapter are synchronised with the `records` field. This may not
+     * be the case if records has recently changed, or if we have experienced a state change.
+     */
+    public void updateRecordList() {
+        if (currentState != State.LIST) {
+            return;
+        }
+
+        clear();
+
+        setNotifyOnChange(false);    // So we don't notify for each add.
+        if (AppConstants.Versions.feature11Plus) {
+             addAll(records);
+        } else {
+            for (ParcelableClientRecord record : records) {
+                add(record);
+            }
+        }
+
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public View getView(final int position, View convertView, ViewGroup parent) {
+        final Context context = getContext();
+
+        // Reuse View objects if they exist.
+        TextView row = (TextView) convertView;
+        if (row == null) {
+            row = (TextView) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
+        }
+
+        if (currentState != State.LIST) {
+            // If we're in a special "Button-like" state, use the override string and a generic icon.
+            row.setText(dummyRecordName);
+            row.setCompoundDrawablesWithIntrinsicBounds(R.drawable.overlay_send_tab_icon, 0, 0, 0);
+        }
+
+        // If we're just a button to launch the dialog, set the listener and abort.
+        if (currentState == State.SHOW_DEVICES) {
+            row.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    dialog.show();
+                }
+            });
+
+            return row;
+        }
+
+        // The remaining states delegate to the SentTabTargetSelectedListener.
+        final String listenerGUID;
+
+        ParcelableClientRecord clientRecord = getItem(position);
+        if (currentState == State.LIST) {
+            row.setText(clientRecord.name);
+            row.setCompoundDrawablesWithIntrinsicBounds(getImage(clientRecord), 0, 0, 0);
+
+            listenerGUID = clientRecord.guid;
+        } else {
+            listenerGUID = null;
+        }
+
+        row.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                listener.onSendTabTargetSelected(listenerGUID);
+            }
+        });
+
+        return row;
+    }
+
+    private static int getImage(ParcelableClientRecord record) {
+        if ("mobile".equals(record.type)) {
+            return R.drawable.sync_mobile;
+        }
+
+        return R.drawable.sync_desktop;
+    }
+
+    public void switchState(State newState) {
+        if (currentState == newState) {
+            return;
+        }
+
+        currentState = newState;
+
+        switch (newState) {
+            case LIST:
+                updateRecordList();
+                break;
+            case NONE:
+                showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_tab_btn_label));
+                break;
+            case SHOW_DEVICES:
+                showDummyRecord(getContext().getResources().getString(R.string.overlay_share_send_other));
+                break;
+            default:
+                Assert.isTrue(false, "Unexpected state transition: " + newState);
+        }
+    }
+
+    /**
+     * Set the dummy override string to the given value and clear the list.
+     */
+    private void showDummyRecord(String name) {
+        dummyRecordName = name;
+        clear();
+        add(null);
+        notifyDataSetChanged();
+    }
+
+    public void setDialog(AlertDialog aDialog) {
+        dialog = aDialog;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/ui/SendTabList.java
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
+
+import java.util.Arrays;
+
+import static org.mozilla.gecko.overlays.ui.SendTabList.State.LIST;
+import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
+import static org.mozilla.gecko.overlays.ui.SendTabList.State.NONE;
+import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
+
+/**
+ * The SendTab button has a few different states depending on the available devices (and whether
+ * we've loaded them yet...)
+ *
+ * Initially, the view resembles a disabled button. (the LOADING state)
+ * Once state is loaded from Sync's database, we know how many devices the user may send their tab
+ * to.
+ *
+ * If there are no targets, the user was found to not have a Sync account, or their Sync account is
+ * in a state that prevents it from being able to send a tab, we enter the NONE state and display
+ * a generic button which launches an appropriate activity to fix the situation when tapped (such
+ * as the set up Sync wizard).
+ *
+ * If the number of targets does not MAX_INLINE_SYNC_TARGETS, we present a button for each of them.
+ * (the LIST state)
+ *
+ * Otherwise, we enter the SHOW_DEVICES state, in which we display a "Send to other devices" button
+ * that takes the user to a menu for selecting a target device from their complete list of many
+ * devices.
+ */
+public class SendTabList extends ListView {
+    private static final String LOGTAG = "SendTabList";
+
+    // The maximum number of target devices to show in the main list. Further devices are available
+    // from a secondary menu.
+    public static final int MAXIMUM_INLINE_ELEMENTS = 2;
+
+    private SendTabDeviceListArrayAdapter clientListAdapter;
+
+    // Listener to fire when a share target is selected (either directly or via the prompt)
+    private SendTabTargetSelectedListener listener;
+
+    private State currentState = LOADING;
+
+    /**
+     * Enum defining the states this view may occupy.
+     */
+    public enum State {
+        // State when no sync targets exist (a generic "Send to Firefox Sync" button which launches
+        // an activity to set it up)
+        NONE,
+
+        // As NONE, but disabled. Initial state. Used until we get information from Sync about what
+        // we really want.
+        LOADING,
+
+        // A list of devices to share to.
+        LIST,
+
+        // A single button prompting the user to select a device to share to.
+        SHOW_DEVICES
+    }
+
+    public SendTabList(Context context) {
+        super(context);
+    }
+
+    public SendTabList(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        Assert.isTrue(adapter instanceof SendTabDeviceListArrayAdapter);
+
+        clientListAdapter = (SendTabDeviceListArrayAdapter) adapter;
+        super.setAdapter(adapter);
+    }
+
+    public void setSendTabTargetSelectedListener(SendTabTargetSelectedListener aListener) {
+        listener = aListener;
+    }
+
+    public void switchState(State state) {
+        if (state == currentState) {
+            return;
+        }
+
+        clientListAdapter.switchState(state);
+        if (state == SHOW_DEVICES) {
+            clientListAdapter.setDialog(getDialog());
+        }
+    }
+
+    public void setSyncClients(ParcelableClientRecord[] clients) {
+        if (clients == null) {
+            clients = new ParcelableClientRecord[0];
+        }
+
+        int size = clients.length;
+        if (size == 0) {
+            // Just show a button to set up sync (or whatever).
+            switchState(NONE);
+            return;
+        }
+
+        clientListAdapter.setClientRecordList(Arrays.asList(clients));
+
+        if (size <= MAXIMUM_INLINE_ELEMENTS) {
+            // Show the list of devices inline.
+            switchState(LIST);
+            return;
+        }
+
+        // Just show a button to launch the list of devices to choose one from.
+        switchState(SHOW_DEVICES);
+    }
+
+    /**
+     * Get an AlertDialog listing all devices, allowing the user to select the one they want.
+     * Used when more than MAXIMUM_INLINE_ELEMENTS devices are found (to avoid displaying them all
+     * inline and looking crazy.
+     */
+    public AlertDialog getDialog() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+
+        final ParcelableClientRecord[] records = clientListAdapter.toArray();
+        final String[] dialogElements = new String[records.length];
+
+        for (int i = 0; i < records.length; i++) {
+            dialogElements[i] = records[i].name;
+        }
+
+        builder.setTitle(R.string.overlay_share_select_device)
+               .setItems(dialogElements, new DialogInterface.OnClickListener() {
+                   public void onClick(DialogInterface dialog, int index) {
+                       listener.onSendTabTargetSelected(records[index].guid);
+                   }
+               });
+
+        return builder.create();
+    }
+
+    /**
+     * Prevent scrolling of this ListView.
+     */
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_MOVE) {
+            return true;
+        }
+
+        return super.dispatchTouchEvent(ev);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/ui/SendTabTargetSelectedListener.java
@@ -0,0 +1,18 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+package org.mozilla.gecko.overlays.ui;
+
+/**
+ * Interface for classes that wish to listen for the selection of an element from a SendTabList.
+ */
+public interface SendTabTargetSelectedListener {
+    /**
+     * Called when a row in the SendTabList is clicked.
+     *
+     * @param targetGUID The GUID of the ClientRecord the element represents (if any, otherwise null)
+     */
+    public void onSendTabTargetSelected(String targetGUID);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/overlays/ui/ShareDialog.java
@@ -0,0 +1,284 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.overlays.ui;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.overlays.OverlayConstants;
+import org.mozilla.gecko.overlays.service.OverlayActionService;
+import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
+import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
+import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
+import org.mozilla.gecko.LocaleAware;
+import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
+
+/**
+ * A transparent activity that displays the share overlay.
+ */
+public class ShareDialog extends LocaleAware.LocaleAwareActivity implements SendTabTargetSelectedListener {
+    private static final String LOGTAG = "GeckoShareDialog";
+
+    private String url;
+    private String title;
+
+    // The override intent specified by SendTab (if any). See SendTab.java.
+    private Intent sendTabOverrideIntent;
+
+    // Flag set during animation to prevent animation multiple-start.
+    private boolean isAnimating;
+
+    // BroadcastReceiver to receive callbacks from ShareMethods which are changing state.
+    private final BroadcastReceiver uiEventListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            ShareMethod.Type originShareMethod = intent.getParcelableExtra(OverlayConstants.EXTRA_SHARE_METHOD);
+            switch (originShareMethod) {
+                case SEND_TAB:
+                    handleSendTabUIEvent(intent);
+                    break;
+                default:
+                    throw new IllegalArgumentException("UIEvent broadcast from ShareMethod that isn't thought to support such broadcasts.");
+            }
+        }
+    };
+
+    /**
+     * Called when a UI event broadcast is received from the SendTab ShareMethod.
+     */
+    protected void handleSendTabUIEvent(Intent intent) {
+        sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT);
+
+        SendTabList sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
+
+        ParcelableClientRecord[] clientrecords = (ParcelableClientRecord[]) intent.getParcelableArrayExtra(SendTab.EXTRA_CLIENT_RECORDS);
+        sendTabList.setSyncClients(clientrecords);
+    }
+
+    @Override
+    protected void onDestroy() {
+        // Remove the listener when the activity is destroyed: we no longer care.
+        // Note: The activity can be destroyed without onDestroy being called. However, this occurs
+        // only when the application is killed, something which also kills the registered receiver
+        // list, and the service, and everything else: so we don't care.
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(uiEventListener);
+
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().setWindowAnimations(0);
+
+        Intent intent = getIntent();
+
+        // The URL is usually hiding somewhere in the extra text. Extract it.
+        String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
+        String pageUrl = new WebURLFinder(extraText).bestWebURL();
+
+        if (TextUtils.isEmpty(pageUrl)) {
+            Log.e(LOGTAG, "Unable to process shared intent. No URL found!");
+
+            // Display toast notifying the user of failure (most likely a developer who screwed up
+            // trying to send a share intent).
+            Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
+            toast.show();
+
+            return;
+        }
+
+        setContentView(R.layout.overlay_share_dialog);
+
+        LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
+                                                                 new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
+
+        // Have the service start any initialisation work that's necessary for us to show the correct
+        // UI. The results of such work will come in via the BroadcastListener.
+        Intent serviceStartupIntent = new Intent(this, OverlayActionService.class);
+        serviceStartupIntent.setAction(OverlayConstants.ACTION_PREPARE_SHARE);
+        startService(serviceStartupIntent);
+
+        // If provided, we use the subject text to give us something nice to display.
+        // If not, we wing it with the URL.
+        // TODO: Consider polling Fennec databases to find better information to display.
+        String subjectText = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+        if (subjectText != null) {
+            ((TextView) findViewById(R.id.title)).setText(subjectText);
+        }
+
+        title = subjectText;
+        url = pageUrl;
+
+        // Set the subtitle text on the view and cause it to marquee if it's too long (which it will
+        // be, since it's a URL).
+        TextView subtitleView = (TextView) findViewById(R.id.subtitle);
+        subtitleView.setText(pageUrl);
+        subtitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+        subtitleView.setSingleLine(true);
+        subtitleView.setMarqueeRepeatLimit(5);
+        subtitleView.setSelected(true);
+
+        // Start the slide-up animation.
+        Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
+        findViewById(R.id.sharedialog).startAnimation(anim);
+
+        // Add button event listeners.
+
+        findViewById(R.id.overlay_share_bookmark_btn).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                addBookmark();
+            }
+        });
+
+        findViewById(R.id.overlay_share_reading_list_btn).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                addToReadingList();
+            }
+        });
+
+        // Send tab.
+        SendTabList sendTabList = (SendTabList) findViewById(R.id.overlay_send_tab_btn);
+
+        // Register ourselves as both the listener and the context for the Adapter.
+        SendTabDeviceListArrayAdapter adapter = new SendTabDeviceListArrayAdapter(this, this, R.layout.sync_list_item);
+        sendTabList.setAdapter(adapter);
+        sendTabList.setSendTabTargetSelectedListener(this);
+    }
+
+    /**
+     * Helper method to get an overlay service intent populated with the data held in this dialog.
+     */
+    private Intent getServiceIntent(ShareMethod.Type method) {
+        final Intent serviceIntent = new Intent(this, OverlayActionService.class);
+        serviceIntent.setAction(OverlayConstants.ACTION_SHARE);
+
+        serviceIntent.putExtra(OverlayConstants.EXTRA_SHARE_METHOD, (Parcelable) method);
+        serviceIntent.putExtra(OverlayConstants.EXTRA_URL, url);
+        serviceIntent.putExtra(OverlayConstants.EXTRA_TITLE, title);
+
+        return serviceIntent;
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+
+        // Don't perform an activity-dismiss animation.
+        overridePendingTransition(0, 0);
+    }
+
+    /*
+     * Button handlers. Send intents to the background service responsible for processing requests
+     * on Fennec in the background. (a nice extensible mechanism for "doing stuff without properly
+     * launching Fennec").
+     */
+
+    public void sendTab(String targetGUID) {
+        // If an override intent has been set, dispatch it.
+        if (sendTabOverrideIntent != null) {
+            startActivity(sendTabOverrideIntent);
+            finish();
+            return;
+        }
+
+        // targetGUID being null with no override intent should be an impossible state.
+        Assert.isTrue(targetGUID != null);
+
+        Intent serviceIntent = getServiceIntent(ShareMethod.Type.SEND_TAB);
+
+        // Currently, only one extra parameter is necessary (the GUID of the target device).
+        Bundle extraParameters = new Bundle();
+
+        // Future: Handle multiple-selection. Bug 1061297.
+        extraParameters.putStringArray(SendTab.SEND_TAB_TARGET_DEVICES, new String[] { targetGUID });
+
+        serviceIntent.putExtra(OverlayConstants.EXTRA_PARAMETERS, extraParameters);
+
+        startService(serviceIntent);
+        slideOut();
+    }
+
+    @Override
+    public void onSendTabTargetSelected(String targetGUID) {
+        sendTab(targetGUID);
+    }
+
+    public void addToReadingList() {
+        startService(getServiceIntent(ShareMethod.Type.ADD_TO_READING_LIST));
+        slideOut();
+    }
+
+    public void addBookmark() {
+        startService(getServiceIntent(ShareMethod.Type.ADD_BOOKMARK));
+        slideOut();
+    }
+
+    /**
+     * Slide the overlay down off the screen and destroy it.
+     */
+    private void slideOut() {
+        if (isAnimating) {
+            return;
+        }
+
+        isAnimating = true;
+        Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_down);
+        findViewById(R.id.sharedialog).startAnimation(anim);
+
+        anim.setAnimationListener(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+                // Unused. I can haz Miranda method?
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                finish();
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {
+                // Unused.
+            }
+        });
+    }
+
+    /**
+     * Close the dialog if back is pressed.
+     */
+    @Override
+    public void onBackPressed() {
+        slideOut();
+    }
+
+    /**
+     * Close the dialog if the anything that isn't a button is tapped.
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        slideOut();
+        return true;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_slide_down.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="@android:integer/config_longAnimTime"
+    android:fromYDelta="0"
+    android:toYDelta="100%p" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/anim/overlay_slide_up.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="@android:integer/config_longAnimTime"
+    android:fromYDelta="100%p"
+    android:toYDelta="0" />
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b8536484cd5737f5316ee25397f5e5cb980c0e0b
GIT binary patch
literal 652
zc%17D@N?(olHy`uVBq!ia0vp^+91rq3?#2LH+}+AnE^f_u0T32F+DCRJw7=Dh=7cc
z=s2&CNEH>8_#}{cY+`C0m=Tkh8XcD!9+w=K1XKwW$%so%2dRN-Plu?FOG=AQOpZ+g
zD@aO@N=T1M02%&q!XZ|mWlAMMe!&dB_s;$(<zr`jG2iyzHUl})_usB&uL(c8e{xTP
zQb)rybywHf;!_zI7&myjIEGl9?w#^-R+9mb>q6ZSGtuv?Znp3LPcY^^#1Zn~ALFOF
zYUY}`4<_!N>MpGl%(dk71)k+Og4H75cCgf5da<*imcjl&vfSNw4(w88w<E7DwP97C
zUc<MQb4BQepb2Nr{$P;V^?Zs-yyXAAN0vUBAH;FxQyB9@m1?%jU-p|8{4-nqreLp%
z!>^CwD$`#p-u||-Jz&wFoA2WF?r(Z@aSg}!FCitCTvfN)9i5iX^;=x<J6^vhXz$`+
zp*Pm6qJFkTlxlx7TKY%S=k-)`;XRB`tAt{wA2=IoX{A4#v*dA=RhM&hVcMhh8E>L?
z-RSS%{I}^=vHn%Jt**y}y0eT8>c4Kf9{74DqY}RqV_LyKf6FK8m2>&sjSel$yH^yp
zXdh3RO~C`Zu6vDVE94t5R4==BFXP!>cIW)9St4tX%v|5S<7@Do`>o0O$4zIHPuH9f
zKf9GJ>wfAwmEc?f)$;Gn8uvxx8~)8M3b}Mr@?ro_Am=(iFNMEcqP>EihEtf<wuY?R
z@z*fPrE_cJt;4Zy-)z@4x$XWa)_vyfW1UXH^J?jj^xw80JyBtuoa<wF@8>E5x9v;o
Z81)`n?ckbNxD6Q744$rjF6*2UngHo;AU^;A
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..19a1374fee79eae7bc9fa8f3073a4492e60a9c54
GIT binary patch
literal 391
zc%17D@N?(olHy`uVBq!ia0vp^+CZ$p!3-q#xD*-!sqg@w5LX}_x7JN7PX<WFtpOtt
z!!>M~UEErC2sd_(Yy4Wb_%%S}3Q-fg$|YuvYwQ}AxK(aZtDGX2*@JX})x@m@Dt2f6
z@z5A(v`k5mUogWTrmvk-uZ8nHoY|lI=5S+avcH{+p^Es9IqT<@FP?uGsP3?*i(`nz
z>8Y1*3N;z<v_8C$BtETaQ}(_8mZeMl9Cjc1|7-S3SO12IVV8IhtU4Ut`s{+xeNl6<
zuN6rb{QLX^KWS>&9nJ2#re5{4nagZ#qp$aenaf_C72n(Pa*e=7S)nb=_mjTs=Xi8+
zm~z!l<+yD3Wma@ql&eO>k~d$xr58lmsFZdcV)ieMPGWXT+;S-RQ1-N-MLhw$ja$wH
zWo`0!`mU@n(yQ~Wi|WMy-@*mTOQfUOmMT_TIoK7u&#KkC7`^kHz)Mb@CNq99$3M4$
P-eK@`^>bP0l+XkKE)t;s
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bac00e61ee9c42c641603d3fd02c090eb25a6e9f
GIT binary patch
literal 340
zc%17D@N?(olHy`uVBq!ia0vp^+CZ$t!3-on_FS+6Qr-bRA+A6=E-51}DLp76B0eb{
z#7+c~>2b+m5`vO5Vw2K<;<1V8F)_)pu?Z0I_~dj3Fo;b~j!({TEm*uB$dM=s@(X5o
zb(CxRo}MRn)}{RX9O&(=EOv201rwu=^tlshKs5_IT^vI!PQSgfQ^-k?r}d%mp|YL7
zv+n)(6qT0rop{b^zvQel@eHm9<}EROlDDT}=jrAxCye*hyxO4=%rA6w-c&`$H}iC*
z_Vg@_OOBD?-MaRV!NN^jyc^f%zFfPfW@YG&+yCwV{J4E4xBJjWrU%LbPtRJ4%cXlq
zMSuOcM>UmCsI~l+UT2c%LBmehuAcJOB1?)cb2VS;exsiA>|X2!En|`Lk7unFW|ow!
P1NqL=)z4*}Q$iB}j+2R9
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3f5700d47451225022adbd0be1fbc56fd1108307
GIT binary patch
literal 325
zc%17D@N?(olHy`uVBq!ia0vp^+CZ$t!3-on_FS+6QZ@lTA+A6=E-4*Cf>3fsTw;1m
zVj7g2lo6Yd0wg066XTN8ViVG16Vn_V9701w<C8OTuAgKF>J%;s@(X78$+Tn5f((a`
zr|)ZBk)7;o%EBouXO{((Uh3)M7-Dhy?G<mnW&<AAi_cY_oZf!#f1EcLb5l#<(pAdu
zG@6xNJ5!Q#HyqhMQFhhVHK)S7t-P;p+I08Py5s*2=KZ~=W;dlP!#!_9hVS)+i(zZF
zioVwExqMsTYMIitTMlcsO^en(`|RI_7w>c!RqOZ;mdq9{Ke;Dq*0*}LC$gb)xDT`~
zJln$=zvaj0yZ@Zp&MYrF5UPJ)KJ~r0$Ju*dXD0cb6F1q**kPwt*0R+G6g&)`u6{1-
HoD!M<3~Ys~
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c9489e49e2c49527849c2c9c24ef3ea561c5b166
GIT binary patch
literal 477
zc%17D@N?(olHy`uVBq!ia0vp^G9b*s3?yAI>n{UR2?0JKu0T32DIJV5A`_FsW0OGa
z#Pr~Z6dfJwxa2e-H#Q+HJ~<;UIXyNpJt!nHHZc{*28k!90Ll2IjOc{a`1mv+BOa&*
zOae`aO$O=O6FK`2&<^>MAirRS_s71=h@C6tW17+C{bx}Qm&5&=I(1>D$}e^$ZJ!W&
z#nQo=fq{|N)5S4F<9zAF@JUA$cw7W;STujGIP`A!f8Fw#0SVLH<4bIp<TZL}Up5QA
z|BA6cKZk9`euwsJJBp`CUK1^TcgrB`sz>oLuY=l8Cw^iP^H?9EFXFy?TY=MODZO<W
zo4;?VUdzfbnSHJ_gUj{rK`+nWjK8v3?_Y5=dqlgY`+TbdZ%;`?E>XVjyE1adC*{-X
zDRr|rw%NSNnONR(aKY~>U!{y|Ty7V>7A{+yy*S2t+6u8<rm`s?11oY)X&n~0+OSe+
zhvU3AjL}TdPqzMA9iLHtf2s5F{)b{=*LUX^Us;vI<M7-l^l@2Yw^PC%ekq^L3FV2_
zJH#F;)~Ua^w8K)t`{%xwttK_8%hguZe?Mtw{hvXTL3rC#rl&K3VaVX=>gTe~DWM4f
D7skex
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a130d78f21582b244a02418dc2972941293e0bf5
GIT binary patch
literal 308
zc%17D@N?(olHy`uVBq!ia0vp^GC(ZK!3-pm?$qT0DenNE5LX}_x7ICYl?#xJUhNDd
z<JN$<(Lj+kZhmtO<JY(W5s)1Zlv?W+y9S6{W7fFDEOv}r>l(MlHGYk2=qi`^weEXe
zR6(Xolmz(&GdxrMpz->#ss0?cn<rL%+t=cvAl{K5$^TxhP6eo@!PCVtMC1I@(~)A$
z1{@B7LOruiE-Xv<ubu0al^giy<!`2UjD4FqYXjC8x|pzC_4;>&!@uX!y*F7m{w6<^
z`n>4xQ=?gaGgh~EPJj3>+qS@C>&ggi9;Lmf|DH>b^LQflBSe0i&&IW?n#WlGr;4R)
rzZzwBv8SuhY~LHE<F<RGm)bJA*$OpZ->IVmbPI#0tDnm{r-UW|*lBva
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6c42af75e4cfb724526d6ca1478f1d9e675ed200
GIT binary patch
literal 336
zc%17D@N?(olHy`uVBq!ia0vp^GC(ZO!3-qzMee5pDZc=p5LX}_my{8ooF0>q5toz>
zL@^1eKyGXzkPQ-viBFA<O@m0qC8h(V;*!(jlYl5KIw94?B@Jjod@@KSki5&*+79GP
zmIV0)Gb}pb@a2z5Q{C=Ks!~^bl3qOB=w`^ycszFk?~`RfMKe5I978nDuRVK_uUUbo
z_2HtWF9ZVS<n8(2-Dhf&skA%dQF-~F;Kr7v8)tv)zPTpH=Ya1T{k;OOrZ42&Hjh0r
zThmlZ;PeD>0XO$`>@i72J?r%f^?G()6t><KSh9Hb%=gD~Zf=%7`rlCb^?x(IGxvX`
z%{ap0FuA*by+qa)*G^CU==G*o4j=GL-fX?*>f22(S<dErd=TF2_?v-WSY*3D=k^Mq
P8yP%Z{an^LB{Ts5F;0ZH
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..24a8aa66b0eef273def2b7ffa1ffcfdd14f97636
GIT binary patch
literal 307
zc%17D@N?(olHy`uVBq!ia0vp^GC(ZO!3-qzMee5pDc1m>5LX}_pOhX4LK#3ZIw3VK
z8N`T7NQ;e21q*_>u}P^>G08xNgM)8eGDv-NY$`}&Vp@E1hV9+9Ye0j<N`m}?8MYi=
zG|S*~Z<N~W#|18;KUr@z%dhi`@ti9QRMGC~;uxZF{^%u7zQYOvE*B5+2|PRb?zg<D
zhMU)hhPC&<Z4a5vrnJ~Fv2k)5%Llb2<u(peuAVpkuBmTL#OgjSp7BEHj)K<RRWi9n
zdpj(|17}wDq-~5`VYJ9?f2qmE?@rp{9%8#Z9nP?`$4vNlbuw4RoWoP@-<0p3m$d)>
kNx^cPe6<%P5&h4Y1w#d2q{|;z33L*Jr>mdKI;Vst02V`Z4gdfE
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4840c7298a157c7a5cc2f6cd6262ab5cdcada750
GIT binary patch
literal 755
zc%17D@N?(olHy`uVBq!ia0vp^79h;Q3?%1AoYnzS2?0JKu0T3AF*PnZBOo|BE;&6e
z35YU)WN1u+yL(VnLK=h%LQvWCm?WTJI!G5-aXd^Mf?|`>fG9d4B`z^7J~`tZb9Fh;
z4*8NGzhDNVKb&{JyonWJk>5Ds>B=kZ9;G(__MMoUy1h0aOC!>}=kgK;1}0@s7sn8f
z<8woAZfjQHDRE>Ky}m2^=l}hCR398;oMO!%>!aT2Y4r7sN5Gz{{|<-iX56i`WBzgO
zh?4+!c$^eR(gZ7^Vzr68KNfs>9Pq2UbAxc<v!hbwiOzxES9oPRl<)Z6D=AT!wqW^{
zZ8hgtFKl)<e|`N3U)r-7^Ur18vQj;6eD3qy<z+Lr1zmh8xNyGD9*?3N)&FLy`*zMw
ziGH?d%4Xk_A7`A()bFmm*-^A(m*lUG*b8^Wez{!X_toXUCuSHd`0)0g)g9f}J|FA<
zW#JH6^m!f+tIxsoFL_U2OljtkI{B&dURRYz-HZ0ENBO;Pt#j7rR?Khx-dCaTXr-`b
zw<k}2=iy9^=f~aeoECW4a#cD`)n@XEWPh*AZ{M9$D_U`PW4N3lm*QdX`Bg9eTtD~w
zkj$6uc8`T-boXv&SbxN;Sny0nuHhmMss5|aHCfZLA8kFbc-GW=NfNgAA2=)RicDI0
z%`YYFcIFnx`*r<u@}pL4UDRKyWqd<icgKsF6FCC6&uN%YzQf4BT;SFk`-{hg7S7gv
zG4IOiB`e$_SC=n(B-49DZH`^r^N#mUYg1~4OmoWizpZf%yZ!g1m;SQYC1Fp`33yKU
z<RA3Vcx~jBr8af<%XU9BcR7(Iw)NdihY891zEz8TF+F`s`JkWllc-4(Hwm9mc(w9)
zStVaB<BL?^{trt!+CM9&uX@k6Am6m^$ou?P?2W=xw;l|u5}1|IJL}lZq>qnvmWi+a
a&wPwI>S}<zRuwR1F?hQAxvX<aXaWEfC|=tD
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7c044394447019738fcfc8d57f017159d1fefaf8
GIT binary patch
literal 422
zc%17D@N?(olHy`uVBq!ia0vp^7C@}U!3-oXx@GJEQhotGA+A7LBAOv?ty}yWx4<Pf
zKn4(nFSc=+q#L)!4F#=*ldjRLougMc#jSA%OS#6bc8gi<9KY6mdgN&vpox+tL4Lsu
zjI$?jPCdxa`uWL?6FV1fT9u$C{im(O%PeHwrV~I#?>t=`Lp+Wjy?VQ^SwW;V(I$v5
zOK|nmzwsqTen#D5t90^z-Ot-EaMDj8CVIV(GHY~b`~5Fxb{VpG{Mr*+bx!ee5udF^
zJJ0h8k7PE@aenQ3J^cEdsp99gi(+5hdcX73-S%V86JE8QI9Yd})!kb9->bYS*}q@R
z;#6POaHi(PvKu=EmT&v-IC<Cf_>Joir`ncz>^*<n?ESml&T4lP;vaqBymX#PeTm=G
zq6Zgp4rMCGOqt^stGX$a|B~Wzz8K!=->%GYzE{MzHTha;fqeAz9%0qoS9eVpt+`pi
xce1hg#kC3f>P6g7FU3Z%&RxItm3iSM{>zCWMT-`&9R~WG!PC{xWt~$(695=xx<>#2
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..436e8bb84ce8a45dbe730779ca424b02ea7b45c8
GIT binary patch
literal 410
zc%17D@N?(olHy`uVBq!ia0vp^7C@}W!3-q5u4}CVQXT<5A+A6=E-5`GAstA@CZ)N%
z`^P0^#3yF}$=JjUkW_pskPT!2$>_M$(AY%pu*BHJ)VSn~nB+8&u7q@;`Zq#XUIWb%
zFA4GsW@vEzlf(S#ds|tu#@V?CWP~`)?+0x1smwS23{>*U)5S5w<M`RDr}J7BL|Pv%
zjR|?T$o<}b-|V7u3F4b(pIY#Aw%&ivHikKVQ*ITt=sptLUZ8VU=MHB~tD+jK@{X@f
z+B)-po^=$tchH|{>1FFLm&;f4pME~0DSq|E$pvo8C#|ohxhJnX)OJ;oQC2a<$^Axx
zv#Tvf>cTT87EYb=w=_c9@{Dw<`#;$tpvdxH-xprIa^bqx`VhIFA^$Byo}9dKbzV}>
zH{D|;69YoLE*^Yfw%_FUnT>JQD*K)O2c#X$liK%Q^Sj5ok^;_v=T|=T)jYoaH{?Oe
u_mgQhOKNtu%AQ|4S2jdIEcOxCJ#nR*QKlKep?iS7X7F_Nb6Mw<&;$S(Cb8uJ
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..56ee92178b127661354e2da15809b1ee22f86976
GIT binary patch
literal 410
zc%17D@N?(olHy`uVBq!ia0vp^7C@}W!3-q5u4}CVQXT<5A+A6=J~1seDLpPJBPJm|
zJ}DzU5lE)T#--cad&Pmo)5D@-AdJ|=6d)NBp9)kQ9i0jmOp61mPtHg^d~+etEb)>c
zzhH(;<962<30tI@FRkF6cHeQo$|s4=I{)(Ne0P9KUU|AWhIkx*d-ZnTAq9bkhm7U7
zigdm_*z<q3p7v_bsn-g9_I%p^=oxn+6XU;2nTw`xx_gvY`q*nF@hV&HrSn+!@~R)z
z%TZw3JzxLNAud^^D8@Cle9!W3-!xXd6l?M6LFc>8HW_!Czv$eUz|1eE*^*Gjxh9As
zQS^|j#T*AC2@f_?&V^YAYOGHDJ^YbPth!bC^<~cxZUc9-9@byG0`nQ#PPA&rq<#uG
zHIcn}Ww>AKx^)W57kT33uUA!AE@BL<6WbX0_WI1lf2(e3XIY$Tk)1wI&T%)}=JbRm
r&UaRv6g{S+JzYoW)|0TiQm6Fu9l~u+J4!YJea+zM>gTe~DWM4fT1KOF
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1a3e2d45d402ae6ceaba4a68b0f4bd3e8e9ab1db
GIT binary patch
literal 1636
zc$@)b2AlbbP)<h;3K|Lk000e1NJLTq002J#002J-1^@s6S6>j{000InNkl<ZXx{Bx
zO>f&q5S>|)iVVo8+W@`g5cJSnf%X^nclBoEW}paC<WN8b?4UgrY0_G*ZE<&JatP@T
zCbP@shw1{A0D^0ZTJgPk^Rcrsz~jCcj;}{yo!$SLfFt-L_#^lu_#^lu_#^lu_^B^c
z=R<#P(wF?#=U#jbzyMtT%fQf39u)w^Ll9Iy66oA@oCs$0=jsCj#{!V9dstr3s#tn|
zy#R0=F}7sS380(+&jq9u5l|lhc#Z&lkIR6?%$N&64iZow7&wZD(BKgOs*LQ>AO@Et
zU~aU>0qbJo-u~Q(XfJ{4v84_a?;$K4?QtS-3w(-@N)%dLXicpI7$6`ySA78BsC+s{
zNTsB2S)CF9W)n~^D0LL+tK=F2zzP6@0lo*#O5QYsfMUz@Kmnq2wPop{>)3sNWdPQ`
zOsXP8b^nmWd2YbVxdIlBU51qY0VzXtNwGD{@;Crct_IN<J3YR=z5U%e2Z$f1VCIK2
z(LYL-*H2GRzchTF40&tPECN0$eQ{!{!-2H+iM&Gy;G6^J+ylV=CBgT=yL+rc2n&m!
zgDz}RU`@-nZ41shsH$r43y7#Eq%z_FWL#r30rd<kJL6o7ai7C6gaB1lJ;=Tge63_p
z-F(3)iYQS@!rTh4RNl@|Km`EUOPQIA2oMF3uIwmN6s6o@mgRHh=};Ai0Dy=%26WEF
ztmk0kxrzv=jhrq6O{HYqA1g6_(#=x?z`8p(0C3X1SHZN`w*U|UU=ILy0N^{x-*@NG
zy%)Qq72_ykMKsnA58!d2hSvTgeGI$Fd(wg+EA-Oc?RNXsYE`Y*>$+*0qzXa^;Jpvt
z`yN~r=V)9bad4&t9^JjQR#~};IrZ7LZN2xNV~l`^(C2)4^l2`Hz}|bGS<RIIOnr8g
z&s$|~?xA#QA^^a*Z{Pm5*=)W5fTzy6s%e^!QV|M>F~WYokGtLO-ObI-JHw)ub;lZL
z5IiSJNzN=lP}JQdWa<C_Uc7km!}I6Qzkc@Y*>`7WXHQQ~PF!8ruwJhr#t1|NZQIiI
z_4OZb-n@Ca-EMzY{u>n;WUK?&AR(Lbs!W7OV?hGO^~xx1<)Q^*jGl<@R;!h3n&$Hm
z!jt`e4@89Cd%C~Br`>M%=jG+)%ZrPPAJ5Ovf4RE4dfz>8=z^U^gdXz<*?&K#!b`36
zQr(t^>#twGe$UKqv)O!hc6Rm^GoMyfRS^+1O+!Q!ce~wR+wFFHadGkMt5>hym&`l2
z(~AT7wMrB&@@NuiuOB-A*mUQWNQVvpz5#%rI?MHGtnL235!d&L^yV|}AyeHaDj2h4
z!-dRR%?KR~N#tEJ4mL{HA#0C$#!=C}H1Kf(3<~yTZSFcN2Vr5$dMR>pfeuPGL&jL1
zMQyFSD<BphQ8g;4lcy7Lqo3za$UG=Aytp2W;HE1%&J;U~@7ScQjsjp$P!zzW<^2T9
z(}2Yn*|A}xW1`1kEIU!DfED;C=J5iowq}X^A=B$0lzu%i*pbm#kX;v6?of#$sL$OF
z0RHYGJc;NnWzUA}V_~3NWUnq)%ChRJNFt(0lTkGq*a&#3bTy^ITMT?^^Uw-_S`pT=
z5;EX}=;Oys1}Ulc7f*FM|2o6kEc}^Nx!hZ^60+Yb@ckUEAR#C9R2{IG<%d>NoM|&V
z;R2FK5|(ErdoHl_EXhyl-g2UEZuz}bOH8z$yZ}!Me2RozfLpY6olAA3tZJ2{dXJ7&
z6Y{Q;Js99MDXgM>%I}LB8DpY-YI8ulg5FaDmD@p);tdN&1!~M1{vbY|3W9p9T|mbI
zT|D^#c|(EJVT$BSIytF;hsF*H8m{4tJwDm=xEI9?B}!si$m+%W^C<#Kf9pnx{s0$P
zo=3`^rvRTC?Mn&Zf?buUY6CLdhi3wxys*h_1g3r}f-2W*z|s}=o%MB=Kt&3}@DG)E
zL~DDN<t+koJ-$?cj4D;?4i6OgcyVf5D%zL+4o(L|Yw<&w9sv}{d=l`f;4K1D(nn)?
zEB(Jsq%fU9hNAkUpQKOOu^CbTCZuTv%xK6vXVtOA%%T-uN%fcWbt_Ix*@v4A{6U6%
i;QwaEiGFq9XTlG09-7iK-TF2F0000<MNUMnLSTYwT@arD
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0d76eda5a494b031bba8df437ffe117f5e8cacdd
GIT binary patch
literal 951
zc%17D@N?(olHy`uVBq!ia0vp^VIa)G3?!2}CLRS+Q2{<7u0Yz;H#|N$JuD_ME-4*|
zViMD05>n#9YzT@^%7C&Hp=4}g8jukcmmHk{Vn|6z0ZjlhA`?@=Dl_7fGgg1JO97fL
zTN30K%)tIr@YYg0gU?UOvtMsW2%X;RaZy_B1ml673e64gr$0N!z`*q0)5S3);_#a5
z-s_qT1Y9l}9TWJcQnPr;_x)$ZCGDphZB%-(xj|}6iK_H|qrX+R(=|5lu6c55cbaIv
z(FGGP1E)a6Kt|iSF0xm2l(xKx3;VmvOnBiLr{mH4_AETK<&o|DM_bvXq%3XZiZT`?
zoOVt)pc1$5;Vwr$+Y6@?50tL1n9As*dT-VB+}{3KtglO|H~-jRF1F^<TiJVaS+h%K
zpZ)NxhgG}ewSa}#nhUv|3sMep-Bn705klNyFQ!FoaN1t7G9lXJ@x4vaNeAEBnYG#)
zp2+KDRX=vQ`OUP5f+QPT5O?`;?{Lp9=8X?pPEB=~=o!VCbkH-h(NpNtJ5z;453fF*
zWT0pmA*C|I$guC*_k;J3NAI}u;AUaF>g2<@2VGCstKVe$BJ@OT`i+>Tf7-LI#{7>n
zIDOXhLas&bYn6h;qi=+Ixcpz=RoJkxQ%e2nMwaf3bro-et2kFlF8H)wcyn%^087&b
zi6v57*7l^iMgRYQ|IO)u?G;5icltSto2$RCd0;2~(Cg|3|Cfr6bN)1on>~H<+F(`m
z{I$EvqW^7u*?2URr$%*2(!pefrO5}A83ZFPNI1xd*|1IMW9?@=0u*QP)LB+^RkOeG
zNYnn(GhA$zI(aM{2Nq}u&tI)1B5Ta^aN|~=n-&(@!tNF~ULR_l&$+kZbgy1aHIw~A
z-r#KyZ|+p?>yqNUa&qY(u^;Ta^KKk!Vpb9VXH@34^F-$H_=~$@RL(S)9-m)Pa_h6l
zjC=L5bIv&}+$+Voz27WshqRK&4iAHU_sb%ew=v0c=8L&LY!Tw}QQ2{v?I5SBNC)S9
z_COK82H9EeTjYiOJ}gOAye0pZ@8B}O1mPT&iY@ki$6lK!Kilj#_rr#s?v^co|He<g
zrdW7r@xeRaXFfS*`!AW#&w*b_sAX2lt%odEHcn*FZI5gh4md3ra^WA>DQm;Y=~7$d
zgXVm=a%0io39pWva8SFw-oi2Q+oa6;gPh?#QqkXQ=WdBFeU!dcH1IR4L-FNMH;J9u
P44^FN>gTe~DWM4f!KAHa
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1b013db090f52a570453324bc6c121cb92081d5c
GIT binary patch
literal 509
zc%17D@N?(olHy`uVBq!ia0vp^p+M}w!3-p03{E=$DenNE5LX}_x7ICgjT?}RUF~Yq
zs|;iV86Xi514Krza`j%U2Ug}9y~-tQxdU7aL@<7>Tl`x0ukDt+K;tAzg8YIR-v9pQ
z!rW(B(&#r+k8N>;yx`SSJJzN>DOEqv#=yYn>FMGa5^?zLwcA-O1|qEwWrh0}C-e24
z{B57<EtwXWb4WtG)#-osXT_WwodrdUuWRx6&G6N#6|Gj<W%hEWS;&{8-&{mi&g*QN
zJ5wXV+ht<D{vzQEEoDkwW=dTmg08%Rt`j>JSqr+>>Sl4hSSxk4vLyN+*RLP$KQn%N
zuP{Bf>7wS6^M^v*elu0sFN&?Yw$<m$)n^eeW(a*UYhM-oV#2<qJj{3b#gD{wPcwhI
zc5lb7$IoX(N*>jpqTc%FkC~Reh|sCCId<J%-yDpuEMCN|J}oMw=9GAhs#dI)$aAZZ
zHF_JXG#1Q#ay!`7VRMeN%fvtzkw|}+iMKm6vaHp*!hBr<<?p+?TTbM45z*yW>vGfU
znkg7);;z&cqp&DrXZl4)Jrffb5lL5<i*HtHOp_2jD)e~Kj*1-`X>H5ZwRH?lIX9=?
gUG#14)XlXFJ#Tk!+^D|X2^jDUp00i_>zopr0Q_^$egFUf
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..51e954466cd9c67185409693a25d802fd108ae17
GIT binary patch
literal 451
zc%17D@N?(olHy`uVBq!ia0vp^VL<G{!3-pIrWu6-DVqSF5LX}_my`}fZvGJvMoe5v
zTv7%YrN<?v$AbmI?6laV^w`AIh^U09_@wyc4C_~G0)hI4OM?7@8D8yXzul&MVqsO}
z_w|$Aa*Tx}KJ9b*#=yX+?CIhd5^?zL_0w5L40u`}DxdtQyK!>Qz5lV+&6;l=oVRXR
zxbWY#NAV}w7$+QAGCizlPsqXLyEZHi)fZGhc=_R%@+)Z*bq~K|II_~Sf+6C{5za|c
z7aLc(ajpsTJF}T5XmiFq)fWPbH}6nbzP@w+wHe=E`5$C0-u7{$>vUn?`8pGB^(`zG
zi>?;jySenM!~|R8s`T0R)33^(ee?hHz3lw-vVWY$*8f|6r@h|_WW?V*(|0OPS?}k^
zx>+J>Tzig2occ6n+Vr4D$Ii{ljj_6T<-*q~apg9iZ|WB2Gr5>DtrYbRQ)XHn$(eQJ
zMrngA$E{d~2hQJD9Y4KqVwv-8KIV^=m(AzwUBLcdSBCG)WY5_eFHY_bb4~9LH|Jn0
k3Ge^+N6OI2F5xTV*J=COWSQQv07H|()78&qol`;+0L^yAh5!Hn
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ef6999aea1388fd2195132d812ce7f8c99dc20d9
GIT binary patch
literal 460
zc$@*u0W<!IP)<h;3K|Lk000e1NJLTq00341002Y?0{{R3ES54=0000vP)t-s0001A
zVrNoRRb60aUSnroVrO1sXJ2AxUSelkTU%aYW?y1wU1DZjTxDNlW?f=uP*6}_VrO4t
zXtJoJh5!Hn5_D2dQvkmRHT<pK^yd$D(rg}yMFg6uPGdd*00A&bL_t(o!|mA1ZiFxl
zKv4(h{Q$=Q|FIpZ&=e#>Nt{JTxl0yD-e4txJye!hvI^lRFNrPbcNMeB;qLP)VohzV
z=4JK95_A8Uh2{o3*uf5Vu%E<!H&|Xa>Wkx-5wU@_be*baXDSq{*+~>wQOy=zQ_Ys$
zYBsy9Y<5}M?6R`iWoEO>&w?{OSl4CR6sQB6G<9InQU?|dbzs*(J=lfHY~^*&_hBO)
zGdwKPA>l+btKr>?eia-{qLo$WA6KWmcdA*5aCB|T`>dLk{<LTe<~^C&`Ufkg=v~-8
zWEWb)=$94X=t^KLyBheLW*ebfNO$>)kr#b~%J^{1Kz~RhJZ$ym4wbWm=Otx|s7g?m
z^ZE+4bApDJS(q~^!;H|}ivFcUu+mr^)GIC%l=}jScB8_c?Mq4k0000<MNUMnLSTZa
Cy2zaX
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_dialog.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- Serves to position the content on the screen (bottom, centered) and provide the drop-shadow -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/sharedialog"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="15dp"
+    android:layout_marginRight="15dp"
+    android:layout_marginBottom="-12dp"
+    android:paddingTop="30dp"
+    android:layout_gravity="bottom|center"
+    android:clipChildren="false"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:id="@+id/share_overlay_content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingTop="8dp"
+        android:orientation="vertical"
+        android:background="@drawable/share_overlay_background">
+
+        <!-- Header -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="0dp"
+            android:background="@color/background_light"
+            android:orientation="vertical"
+            android:paddingTop="10dp"
+            android:paddingBottom="15dp"
+            android:paddingLeft="15dp"
+            android:paddingRight="15dp"
+            android:layout_gravity="center">
+
+            <!-- Title -->
+            <TextView
+                android:id="@id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingBottom="7dp"
+                android:ellipsize="end"
+                android:maxLines="2"
+                android:scrollHorizontally="true"
+                android:textColor="@color/text_color_primary"
+                android:textSize="17sp"/>
+
+            <!-- Subtitle (url) -->
+            <TextView
+                android:id="@+id/subtitle"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textColor="@color/text_color_secondary"/>
+
+        </LinearLayout>
+
+        <!-- Buttons -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/overlay_share_background_colour"
+            android:orientation="vertical">
+
+            <!-- TODO: Once API 11 is available, stick "showDividers=middle" into the parent and get rid
+                       of these evil separator views. -->
+
+            <!-- "Send to Firefox Sync" -->
+            <org.mozilla.gecko.overlays.ui.SendTabList
+                style="@style/ShareOverlayButton"
+                android:id="@+id/overlay_send_tab_btn"
+                android:background="@color/overlay_share_background_colour"
+                android:padding="0dp"/>
+
+            <!-- Evil separator -->
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:background="@color/background_light"/>
+
+            <!-- "Add to reading list" -->
+            <TextView
+                style="@style/ShareOverlayButton.Text"
+                android:id="@+id/overlay_share_reading_list_btn"
+                android:text="@string/overlay_share_reading_list_btn_label"
+                android:drawableLeft="@drawable/overlay_readinglist_icon"/>
+
+            <!-- Evil separator -->
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:background="@color/background_light"/>
+
+            <!-- "Add bookmark" -->
+            <TextView
+                style="@style/ShareOverlayButton.Text"
+                android:id="@+id/overlay_share_bookmark_btn"
+                android:text="@string/overlay_share_bookmark_btn_label"
+                android:drawableLeft="@drawable/overlay_bookmark_icon"/>
+
+        </LinearLayout>
+    </LinearLayout>
+
+    <!-- Firefox logo (has to appear higher in the z-order than the content. -->
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_above="@+id/share_overlay_content"
+        android:scaleType="center"
+        android:layout_centerHorizontal="true"
+        android:src="@drawable/icon"
+        android:layout_marginBottom="-6dp"/>
+</RelativeLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_send_tab_button.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <ListView
+        style="@style/ShareOverlayButton"
+        android:id="@+id/device_list"
+        android:padding="0dp" >
+    </ListView>
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_send_tab_item.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/ShareOverlayButton.Text"/>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/overlay_share_toast.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/overlay_share_toast"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:background="@drawable/share_overlay_background"
+    android:layout_marginLeft="5dp"
+    android:layout_marginRight="5dp"
+    android:layout_gravity="bottom|center">
+
+    <!-- Header -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="0dp"
+        android:background="@color/background_light"
+        android:orientation="horizontal"
+        android:paddingLeft="5dp"
+        android:paddingRight="10dp"
+        android:paddingTop="5dp"
+        android:paddingBottom="5dp">
+
+        <!-- Large attractive green tick with label to the right -->
+        <TextView
+            style="@style/ShareOverlayButton.Text"
+            android:id="@+id/overlay_toast_message"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:textColor="@color/text_color_primary"
+            android:textSize="14sp"
+            android:drawableLeft="@drawable/overlay_check"/>
+
+        <!-- Evil separator -->
+        <View
+            android:id="@+id/overlay_toast_separator"
+            android:layout_marginTop="15dp"
+            android:layout_marginBottom="15dp"
+            android:layout_height="match_parent"
+            android:layout_width="1dp"
+            android:background="@color/background_light"/>
+
+        <!-- Retry button -->
+        <Button
+            android:id="@+id/overlay_toast_retry_btn"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:text="@string/overlay_share_retry"
+            android:onClick="selfDestruct" />
+
+    </LinearLayout>
+</LinearLayout>
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -43,16 +43,22 @@
   <color name="text_color_secondary">#777777</color>
   <color name="text_color_tertiary">#9198A1</color>
 
   <!-- Default inverse colors -->
   <color name="text_color_primary_inverse">#FFFFFF</color>
   <color name="text_color_secondary_inverse">#DDDDDD</color>
   <color name="text_color_tertiary_inverse">#A4A7A9</color>
 
+  <!-- Colour used for share overlay button labels -->
+  <color name="text_color_overlaybtn">#666666</color>
+
+  <!-- Colour used for share overlay button background -->
+  <color name="overlay_share_background_colour">#FFD0CECB</color>
+
   <!-- Disabled colors -->
   <color name="text_color_primary_disable_only">#999999</color>
 
   <!-- Hint colors -->
   <color name="text_color_hint">#666666</color>
   <color name="text_color_hint_inverse">#7F828A</color>
   <color name="text_color_hint_floating_focused">#33b5e5</color>
 
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -758,16 +758,39 @@
     </style>
 
     <style name="GeckoActionBar.Buttons">
         <item name="android:background">@android:color/transparent</item>
         <item name="android:textColor">@color/text_color_primary</item>
         <item name="android:gravity">right</item>
     </style>
 
+    <style name="ShareOverlayButton">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:minHeight">60dp</item>
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:paddingLeft">15dp</item>
+        <item name="android:paddingRight">15dp</item>
+        <item name="android:paddingTop">17dp</item>
+        <item name="android:paddingBottom">17dp</item>
+        <item name="android:focusableInTouchMode">false</item>
+        <item name="android:clickable">true</item>
+        <item name="android:background">@android:drawable/list_selector_background</item>
+        <item name="android:layout_margin">0dp</item>
+    </style>
+
+    <style name="ShareOverlayButton.Text">
+        <item name="android:drawablePadding">15dp</item>
+        <item name="android:maxLines">1</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">@color/text_color_overlaybtn</item>
+        <item name="android:ellipsize">marquee</item>
+    </style>
+
     <style name="TabInput"></style>
 
     <style name="TabInput.TabWidget">
         <item name="android:divider">@drawable/divider_vertical</item>
         <item name="android:background">@drawable/tab_indicator_background</item>
     </style>
 
     <style name="TabInput.Tab">
@@ -775,16 +798,23 @@
         <item name="android:gravity">center</item>
         <item name="android:minHeight">@dimen/menu_item_row_height</item>
     </style>
 
     <style name="FloatingHintEditText" parent="android:style/Widget.EditText">
         <item name="android:paddingTop">0dp</item>
     </style>
 
+    <!-- Make the share overlay activity appear like an overlay. -->
+    <style name="ShareOverlayActivity">
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:backgroundDimEnabled">true</item>
+    </style>
     <style name="OnboardStartLayout">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
     </style>
 
     <style name="OnboardStartTextAppearance">
         <item name="android:textColor">#5F636B</item>
     </style>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -105,16 +105,25 @@
   <string name="find_next">&find_next;</string>
   <string name="find_close">&find_close;</string>
 
   <string name="media_casting_to">&media_casting_to;</string>
   <string name="media_play">&media_play;</string>
   <string name="media_pause">&media_pause;</string>
   <string name="media_stop">&media_stop;</string>
 
+  <string name="overlay_share_send_other">&overlay_share_send_other;</string>
+  <string name="overlay_share_header">&overlay_share_header;</string>
+  <string name="overlay_share_bookmark_btn_label">&overlay_share_bookmark_btn_label;</string>
+  <string name="overlay_share_reading_list_btn_label">&overlay_share_reading_list_btn_label;</string>
+  <string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
+  <string name="overlay_share_no_url">&overlay_share_no_url;</string>
+  <string name="overlay_share_retry">&overlay_share_retry;</string>
+  <string name="overlay_share_select_device">&overlay_share_select_device;</string>
+
   <string name="settings">&settings;</string>
   <string name="settings_title">&settings_title;</string>
   <string name="pref_category_advanced">&pref_category_advanced;</string>
   <string name="pref_category_customize">&pref_category_customize;</string>
   <string name="pref_category_search">&pref_category_search3;</string>
   <string name="pref_category_search_summary">&pref_category_search_summary;</string>
   <string name="pref_category_display">&pref_category_display;</string>
   <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
--- a/mobile/android/base/toolbar/BackButton.java
+++ b/mobile/android/base/toolbar/BackButton.java
@@ -44,20 +44,20 @@ public class BackButton extends ShapedBu
         mBorderPaint.setColor(isPrivate ? 0xFF363B40 : 0xFFB5B5B5);
     }
 
     @Override
     protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
         super.onSizeChanged(width, height, oldWidth, oldHeight);
 
         mPath.reset();
-        mPath.addCircle(width/2, height/2, width/2 - mBorderWidth, Path.Direction.CW);
+        mPath.addCircle(width/2, height/2, width/2, Path.Direction.CW);
 
         mBorderPath.reset();
-        mBorderPath.addCircle(width/2, height/2, (width/2) - mBorderWidth, Path.Direction.CW);
+        mBorderPath.addCircle(width/2, height/2, (width/2) - (mBorderWidth/2), Path.Direction.CW);
     }
 
     @Override
     public void draw(Canvas canvas) {
         mCanvasDelegate.draw(canvas, mPath, getWidth(), getHeight());
 
         // Draw the border on top.
         canvas.drawPath(mBorderPath, mBorderPaint);
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -76,10 +76,13 @@ MOZ_WEBGL_CONFORMANT=1
 
 # Enable the Search Activity in nightly.
 if test "$NIGHTLY_BUILD"; then
   MOZ_ANDROID_SEARCH_ACTIVITY=1
 else
   MOZ_ANDROID_SEARCH_ACTIVITY=
 fi
 
+# Don't enable the share overlay.
+# MOZ_ANDROID_SHARE_OVERLAY=1
+
 # Don't enable the Mozilla Location Service stumbler.
 # MOZ_ANDROID_MLS_STUMBLER=1
--- a/mobile/android/services/manifests/SyncAndroidManifest_activities.xml.in
+++ b/mobile/android/services/manifests/SyncAndroidManifest_activities.xml.in
@@ -69,24 +69,26 @@
         <receiver
             android:name="org.mozilla.gecko.sync.receivers.SyncAccountDeletedReceiver"
             android:permission="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE">
             <intent-filter>
                 <action android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.accounts.SYNC_ACCOUNT_DELETED_ACTION"/>
             </intent-filter>
         </receiver>
 
+#ifndef MOZ_ANDROID_SHARE_OVERLAY
         <activity
             android:theme="@style/SyncTheme"
             android:excludeFromRecents="true"
             android:icon="@drawable/icon"
             android:label="@string/sync_title_send_tab"
             android:configChanges="keyboardHidden|orientation|screenSize|locale|layoutDirection"
             android:windowSoftInputMode="adjustResize|stateHidden"
             android:taskAffinity="org.mozilla.gecko.sync.setup"
             android:name="org.mozilla.gecko.sync.setup.activities.SendTabActivity" >
 
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="text/plain" />
             </intent-filter>
         </activity>
+#endif
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -4027,18 +4027,19 @@ DebuggerServer.ObjectActorPreviewers.Obj
       }
       if (aRawObj.shiftKey) {
         modifiers.push("Shift");
       }
       preview.eventKind = "key";
       preview.modifiers = modifiers;
 
       props.push("key", "charCode", "keyCode");
-    } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent ||
-               aRawObj instanceof Ci.nsIDOMAnimationEvent) {
+    } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent) {
+      props.push("propertyName", "pseudoElement");
+    } else if (aRawObj instanceof Ci.nsIDOMAnimationEvent) {
       props.push("animationName", "pseudoElement");
     } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) {
       props.push("clipboardData");
     }
 
     // Add event-specific properties.
     for (let prop of props) {
       let value = aRawObj[prop];