Merge m-i and f-t to m-c, a=merge FIREFOX_AURORA_34_BASE
authorPhil Ringnalda <philringnalda@gmail.com>
Mon, 01 Sep 2014 22:45:56 -0700
changeset 202941 c360f3d1c00d73b0c1fb0a2c0da525cb55e58b83
parent 202931 3b1a82e632f18a385fe19543ab4005615d889c74 (current diff)
parent 202940 f8f3a9b2f9ed0feedde6c98fda7d5f0fc04f375b (diff)
child 202942 1cdc9fca4297db04d66453b2f14d26fa82b21ad6
child 202945 8fc296b9a1df145702c2988757fa6c0a14efde36
child 203030 4f308fdd6ada6470a3431d9a75072b091a3df3a9
push idunknown
push userunknown
push dateunknown
reviewersmerge
milestone34.0a1
Merge m-i and f-t to m-c, a=merge
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -96,17 +96,17 @@ loop.Client = (function($) {
     },
 
     /**
      * Internal handler for requesting a call url from the server.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
-     * -- call_url: The url of the call
+     * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     _requestCallUrlInternal: function(nickname, cb) {
       this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname},
@@ -115,18 +115,16 @@ loop.Client = (function($) {
           this._failureHandler(cb, error);
           return;
         }
 
         try {
           var urlData = JSON.parse(responseText);
 
           cb(null, this._validate(urlData, expectedCallUrlProperties));
-
-          this.mozLoop.noteCallUrlExpiry(urlData.expiresAt);
         } catch (err) {
           console.log("Error requesting call info", err);
           cb(err);
         }
       }.bind(this));
     },
 
     /**
@@ -154,33 +152,31 @@ loop.Client = (function($) {
                                function (error, responseText) {
         if (error) {
           this._failureHandler(cb, error);
           return;
         }
 
         try {
           cb(null);
-
-          this.mozLoop.noteCallUrlExpiry((new Date()).getTime() / 1000);
         } catch (err) {
           console.log("Error deleting call info", err);
           cb(err);
         }
       }.bind(this));
     },
 
     /**
      * Requests a call URL from the Loop server. It will note the
      * expiry time for the url with the mozLoop api.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
-     * -- call_url: The url of the call
+     * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     requestCallUrl: function(nickname, cb) {
       this._ensureRegistered(function(err) {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -254,26 +254,28 @@ loop.panel = (function(_, mozL10n) {
           )
         )
       );
     }
   });
 
   var CallUrlResult = React.createClass({displayName: 'CallUrlResult',
     propTypes: {
-      callUrl:  React.PropTypes.string,
-      notifier: React.PropTypes.object.isRequired,
-      client:   React.PropTypes.object.isRequired
+      callUrl:        React.PropTypes.string,
+      callUrlExpiry:  React.PropTypes.number,
+      notifier:       React.PropTypes.object.isRequired,
+      client:         React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         pending: false,
         copied: false,
-        callUrl: this.props.callUrl || ""
+        callUrl: this.props.callUrl || "",
+        callUrlExpiry: 0
       };
     },
 
     /**
     * Returns a random 5 character string used to identify
     * the conversation.
     * XXX this will go away once the backend changes
     */
@@ -302,60 +304,72 @@ loop.panel = (function(_, mozL10n) {
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
-          this.setState({pending: false, copied: false, callUrl: callUrl.href});
+          this.setState({pending: false, copied: false,
+                         callUrl: callUrl.href,
+                         callUrlExpiry: callUrlData.expiresAt});
         } catch(e) {
           console.log(e);
           this.props.notifier.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     _generateMailTo: function() {
       return encodeURI([
         "mailto:?subject=" + __("share_email_subject3") + "&",
         "body=" + __("share_email_body3", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
     handleEmailButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // Note: side effect
       document.location = event.target.dataset.mailto;
     },
 
     handleCopyButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
+    handleLinkExfiltration: function(event) {
+      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      if (this.state.callUrlExpiry) {
+        navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
+      }
+    },
+
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
       var inputCSSClass = cx({
         "pending": this.state.pending,
         // Used in functional testing, signals that
         // call url was received from loop server
          "callUrl": !this.state.pending
       });
       return (
         PanelLayout({summary: __("share_link_header_text")}, 
           React.DOM.div({className: "invite"}, 
             React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
+                   onCopy: this.handleLinkExfiltration, 
                    className: inputCSSClass}), 
             React.DOM.p({className: "btn-group url-actions"}, 
               React.DOM.button({className: "btn btn-email", disabled: !this.state.callUrl, 
                 onClick: this.handleEmailButtonClick, 
                 'data-mailto': this._generateMailTo()}, 
                 __("share_button")
               ), 
               React.DOM.button({className: "btn btn-copy", disabled: !this.state.callUrl, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -254,26 +254,28 @@ loop.panel = (function(_, mozL10n) {
           </div>
         </div>
       );
     }
   });
 
   var CallUrlResult = React.createClass({
     propTypes: {
-      callUrl:  React.PropTypes.string,
-      notifier: React.PropTypes.object.isRequired,
-      client:   React.PropTypes.object.isRequired
+      callUrl:        React.PropTypes.string,
+      callUrlExpiry:  React.PropTypes.number,
+      notifier:       React.PropTypes.object.isRequired,
+      client:         React.PropTypes.object.isRequired
     },
 
     getInitialState: function() {
       return {
         pending: false,
         copied: false,
-        callUrl: this.props.callUrl || ""
+        callUrl: this.props.callUrl || "",
+        callUrlExpiry: 0
       };
     },
 
     /**
     * Returns a random 5 character string used to identify
     * the conversation.
     * XXX this will go away once the backend changes
     */
@@ -302,60 +304,72 @@ loop.panel = (function(_, mozL10n) {
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
-          this.setState({pending: false, copied: false, callUrl: callUrl.href});
+          this.setState({pending: false, copied: false,
+                         callUrl: callUrl.href,
+                         callUrlExpiry: callUrlData.expiresAt});
         } catch(e) {
           console.log(e);
           this.props.notifier.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     _generateMailTo: function() {
       return encodeURI([
         "mailto:?subject=" + __("share_email_subject3") + "&",
         "body=" + __("share_email_body3", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
     handleEmailButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // Note: side effect
       document.location = event.target.dataset.mailto;
     },
 
     handleCopyButtonClick: function(event) {
+      this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
+    handleLinkExfiltration: function(event) {
+      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      if (this.state.callUrlExpiry) {
+        navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
+      }
+    },
+
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
       var inputCSSClass = cx({
         "pending": this.state.pending,
         // Used in functional testing, signals that
         // call url was received from loop server
          "callUrl": !this.state.pending
       });
       return (
         <PanelLayout summary={__("share_link_header_text")}>
           <div className="invite">
             <input type="url" value={this.state.callUrl} readOnly="true"
+                   onCopy={this.handleLinkExfiltration}
                    className={inputCSSClass} />
             <p className="btn-group url-actions">
               <button className="btn btn-email" disabled={!this.state.callUrl}
                 onClick={this.handleEmailButtonClick}
                 data-mailto={this._generateMailTo()}>
                 {__("share_button")}
               </button>
               <button className="btn btn-copy" disabled={!this.state.callUrl}
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -79,30 +79,16 @@ describe("loop.Client", function() {
            // and the url.
            hawkRequestStub.callsArgWith(3, null);
 
            client.deleteCallUrl(fakeToken, callback);
 
            sinon.assert.calledWithExactly(callback, null);
          });
 
-      it("should reset all url expiry when the request succeeds", function() {
-        // Sets up the hawkRequest stub to trigger the callback with no error
-        // and the url.
-        var dateInMilliseconds = new Date(2014,7,20).getTime();
-        hawkRequestStub.callsArgWith(3, null);
-        sandbox.useFakeTimers(dateInMilliseconds);
-
-        client.deleteCallUrl(fakeToken, callback);
-
-        sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
-        sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
-                                       dateInMilliseconds / 1000);
-      });
-
       it("should send an error when the request fails", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
         hawkRequestStub.callsArgWith(3, fakeErrorRes);
 
         client.deleteCallUrl(fakeToken, callback);
 
         sinon.assert.calledOnce(callback);
@@ -148,33 +134,31 @@ describe("loop.Client", function() {
           hawkRequestStub.callsArgWith(3, null,
             JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", callback);
 
           sinon.assert.calledWithExactly(callback, null, callUrlData);
         });
 
-      it("should note the call url expiry when the request succeeds",
+      it("should not update call url expiry when the request succeeds",
         function() {
           var callUrlData = {
             "callUrl": "fakeCallUrl",
             "expiresAt": 6000
           };
 
           // Sets up the hawkRequest stub to trigger the callback with no error
           // and the url.
           hawkRequestStub.callsArgWith(3, null,
             JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", callback);
 
-          sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
-          sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
-            6000);
+          sinon.assert.notCalled(mozLoop.noteCallUrlExpiry);
         });
 
       it("should send an error when the request fails", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
         hawkRequestStub.callsArgWith(3, fakeErrorRes);
 
         client.requestCallUrl("foo", callback);
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -44,17 +44,18 @@ describe("loop.panel", function() {
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub().returns("unseen"),
-      copyString: sandbox.stub()
+      copyString: sandbox.stub(),
+      noteCallUrlExpiry: sinon.spy()
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     sandbox.restore();
@@ -407,26 +408,89 @@ describe("loop.panel", function() {
         fakeClient.requestCallUrl = sandbox.stub();
         var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
           notifier: notifier,
           client: fakeClient
         }));
         view.setState({
           pending: false,
           copied: false,
-          callUrl: "http://example.com"
+          callUrl: "http://example.com",
+          callUrlExpiry: 6000
         });
 
         TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-copy"));
 
         sinon.assert.calledOnce(navigator.mozLoop.copyString);
         sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
           view.state.callUrl);
       });
 
+      it("should note the call url expiry when the url is copied via button",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifier: notifier,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-copy"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
+          sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+            6000);
+        });
+
+      it("should note the call url expiry when the url is emailed",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifier: notifier,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          view.getDOMNode().querySelector(".btn-email").dataset.mailto = "#";
+          TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-email"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
+          sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+            6000);
+        });
+
+      it("should note the call url expiry when the url is copied manually",
+        function() {
+          var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+            notifier: notifier,
+            client: fakeClient
+          }));
+          view.setState({
+            pending: false,
+            copied: false,
+            callUrl: "http://example.com",
+            callUrlExpiry: 6000
+          });
+
+          var urlField = view.getDOMNode().querySelector("input[type='url']");
+          TestUtils.Simulate.copy(urlField);
+
+          sinon.assert.calledOnce(navigator.mozLoop.noteCallUrlExpiry);
+          sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
+            6000);
+        });
+
       it("should notify the user when the operation failed", function() {
         fakeClient.requestCallUrl = function(_, cb) {
           cb("fake error");
         };
         var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
           notifier: notifier,
           client: fakeClient
         }));
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -369,17 +369,17 @@
             <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:label="@string/overlay_share_label"
                   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" />
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -946,17 +946,20 @@ public class LocalBrowserDB {
         String[] args = new String[] { value };
         int updated  = cr.update(mParentsUriWithProfile, values, where, args);
         debug("Updated " + updated + " rows to new modified time.");
     }
 
     private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) {
         final long now = System.currentTimeMillis();
         ContentValues values = new ContentValues();
-        values.put(Browser.BookmarkColumns.TITLE, title);
+        if (title != null) {
+            values.put(Browser.BookmarkColumns.TITLE, title);
+        }
+
         values.put(Bookmarks.URL, uri);
         values.put(Bookmarks.PARENT, folderId);
         values.put(Bookmarks.DATE_MODIFIED, now);
 
         // Get the page's favicon ID from the history table
         Cursor c = null;
         try {
             c = cr.query(mHistoryUriWithProfile,
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -85,22 +85,26 @@
 <!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_label) : This is the label that appears
+     in Android's intent chooser when sending a link to Firefox to bookmark,
+     send to another device, or add to Reading List. -->
+<!ENTITY overlay_share_label "Add to &brandShortName;">
+
 <!-- 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">
--- a/mobile/android/base/overlays/service/sharemethods/AddBookmark.java
+++ b/mobile/android/base/overlays/service/sharemethods/AddBookmark.java
@@ -14,17 +14,17 @@ import org.mozilla.gecko.db.LocalBrowser
 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);
+        browserDB.addBookmark(resolver, title, url);
 
         return Result.SUCCESS;
     }
 
     public String getSuccessMesssage() {
         return context.getResources().getString(R.string.bookmark_added);
     }
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -106,17 +106,17 @@
   <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_label">&overlay_share_label;</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>
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -76,13 +76,15 @@ 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
+# Enable the share handler in pre-release builds.
+if test ! "$RELEASE_BUILD"; then
+  MOZ_ANDROID_SHARE_OVERLAY=1
+fi
 
 # Don't enable the Mozilla Location Service stumbler.
 # MOZ_ANDROID_MLS_STUMBLER=1