Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 08 Oct 2014 16:46:52 -0700
changeset 209411 a689386944da22b83a337c63052ef0f93fb43f30
parent 209393 7b16babf6a732741631ff2bb4c83eaf6e7b4fc66 (current diff)
parent 209410 d6dbd2f1b73412e0da32e6f44ca3d993e7ef82fa (diff)
child 209460 f0bb13ef0ee488d4ece70467e4e819cb7506f5e3
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmerge
milestone35.0a1
Merge fx-team to m-c a=merge
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -393,18 +393,17 @@ panel[noactions] > richlistbox > richlis
 panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-action-icon {
   visibility: collapse;
 }
 
 panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-url > .ac-url-text {
   visibility: visible;
 }
 
-#urlbar:not([actiontype]) > #urlbar-display-box,
-#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > .urlbar-display-switchtab {
+#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
   display: none;
 }
 
 #PopupAutoComplete {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup");
 }
 
 #PopupAutoCompleteRichResult {
--- a/browser/components/loop/GoogleImporter.jsm
+++ b/browser/components/loop/GoogleImporter.jsm
@@ -420,17 +420,17 @@ this.GoogleImporter.prototype = {
           ["locality", "city"],
           ["postalCode", "postcode"],
           ["region", "region"],
           ["streetAddress", "street"]
         ]), addressNode, kNS_GD);
         if (Object.keys(adr).length) {
           adr.pref = (addressNode.getAttribute("primary") == "true");
           adr.type = [getFieldType(addressNode)];
-          contacts.adr.push(adr);
+          contact.adr.push(adr);
         }
       }
     }
 
     // Process email addresses.
     let emailNodes = entry.getElementsByTagNameNS(kNS_GD, "email");
     if (emailNodes.length) {
       contact.email = [];
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -53,16 +53,23 @@
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   // Local mocks
 
+  var mockContact = {
+    name: ["Mr Smith"],
+    email: [{
+      value: "smith@invalid.com"
+    }]
+  };
+
   var mockClient = {
     requestCallUrl: noop,
     requestCallUrlInfo: noop
   };
 
   var mockSDK = {};
 
   var mockConversationModel = new loop.shared.models.ConversationModel({}, {
@@ -249,17 +256,18 @@
               )
             )
           ), 
 
           Section({name: "PendingConversationView (Desktop)"}, 
             Example({summary: "Connecting", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
-                DesktopPendingConversationView({callState: "gather", calleeId: "Mr Smith"})
+                DesktopPendingConversationView({callState: "gather", 
+                                                contact: mockContact})
               )
             )
           ), 
 
           Section({name: "CallFailedView"}, 
             Example({summary: "Call Failed", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -53,16 +53,23 @@
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   // Local mocks
 
+  var mockContact = {
+    name: ["Mr Smith"],
+    email: [{
+      value: "smith@invalid.com"
+    }]
+  };
+
   var mockClient = {
     requestCallUrl: noop,
     requestCallUrlInfo: noop
   };
 
   var mockSDK = {};
 
   var mockConversationModel = new loop.shared.models.ConversationModel({}, {
@@ -249,17 +256,18 @@
               </div>
             </Example>
           </Section>
 
           <Section name="PendingConversationView (Desktop)">
             <Example summary="Connecting" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
-                <DesktopPendingConversationView callState={"gather"} calleeId="Mr Smith" />
+                <DesktopPendingConversationView callState={"gather"}
+                                                contact={mockContact} />
               </div>
             </Example>
           </Section>
 
           <Section name="CallFailedView">
             <Example summary="Call Failed" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -186,17 +186,17 @@ public class BrowserApp extends GeckoApp
         public boolean checkable;
         public boolean checked;
         public boolean enabled = true;
         public boolean visible = true;
         public int parent;
         public boolean added;   // So we can re-add after a locale change.
     }
 
-    // The types of guest mdoe dialogs we show
+    // The types of guest mode dialogs we show.
     public static enum GuestModeDialog {
         ENTERING,
         LEAVING
     }
 
     private Vector<MenuItemInfo> mAddonMenuItemsCache;
     private PropertyAnimator mMainLayoutAnimator;
 
@@ -515,21 +515,22 @@ public class BrowserApp extends GeckoApp
         }).execute();
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         mAboutHomeStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_ABOUTHOME");
 
         final Intent intent = getIntent();
-        final String args = intent.getStringExtra("args");
-
-        if (GuestSession.shouldUse(this, args)) {
-            mProfile = GeckoProfile.createGuestProfile(this);
-        } else {
+        final GeckoProfile p = GeckoProfile.get(this);
+        if (p != null && !p.inGuestMode()) {
+            // This is *only* valid because we never want to use the guest mode
+            // profile concurrently with a normal profile -- no syncing to it,
+            // no dual-profile usage, nothing. BrowserApp startup with a conventional
+            // GeckoProfile will cause the guest profile to be deleted.
             GeckoProfile.maybeCleanupGuestProfile(this);
         }
 
         // This has to be prepared prior to calling GeckoApp.onCreate, because
         // widget code and BrowserToolbar need it, and they're created by the
         // layout, which GeckoApp takes care of.
         ((GeckoApplication) getApplication()).prepareLightweightTheme();
         super.onCreate(savedInstanceState);
@@ -668,23 +669,16 @@ public class BrowserApp extends GeckoApp
         if (mediaManagerClass != null) {
             try {
                 Method init = mediaManagerClass.getMethod("init", Context.class);
                 init.invoke(null, this);
             } catch(Exception ex) {
                 Log.e(LOGTAG, "Error initializing media manager", ex);
             }
         }
-
-        if (getProfile().inGuestMode()) {
-            GuestSession.showNotification(this);
-        } else {
-            // If we're restarting, we won't destroy the activity. Make sure we remove any guest notifications that might have been shown.
-            GuestSession.hideNotification(this);
-        }
     }
 
     private void setupSystemUITinting() {
         if (!Versions.feature19Plus) {
             return;
         }
 
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
@@ -786,16 +780,45 @@ public class BrowserApp extends GeckoApp
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
             "Prompt:ShowTop");
 
         final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
         lbm.unregisterReceiver(mOnboardingReceiver);
     }
 
     @Override
+    public void onStart() {
+        super.onStart();
+
+        // Queue this work so that the first launch of the activity doesn't
+        // trigger profile init too early.
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                if (getProfile().inGuestMode()) {
+                    GuestSession.showNotification(BrowserApp.this);
+                } else {
+                    // If we're restarting, we won't destroy the activity.
+                    // Make sure we remove any guest notifications that might
+                    // have been shown.
+                    GuestSession.hideNotification(BrowserApp.this);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // We only show the guest mode notification when our activity is in the foreground.
+        GuestSession.hideNotification(this);
+    }
+
+    @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         super.onWindowFocusChanged(hasFocus);
         // If Home Page is visible, the layerView surface has to be visible
         // to avoid a surface issue in Gingerbread phones.
         // We need to do this on the next iteration.
         // See bugs: 1058027 and 1003123
         if (mInitialized && hasFocus &&
             Versions.preHC && isHomePagerVisible() &&
@@ -1082,18 +1105,16 @@ public class BrowserApp extends GeckoApp
             mOrderedBroadcastHelper = null;
         }
 
         if (mBrowserHealthReporter != null) {
             mBrowserHealthReporter.uninit();
             mBrowserHealthReporter = null;
         }
 
-        GuestSession.onDestroy(this);
-
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
             "Menu:Update",
             "Reader:Added",
             "Reader:FaviconRequest",
             "Search:Keyword",
             "Prompt:ShowTop",
             "Accounts:Exist");
 
@@ -2934,16 +2955,19 @@ public class BrowserApp extends GeckoApp
                 try {
                     int itemId = new JSONObject(result).getInt("button");
                     if (itemId == 0) {
                         String args = "";
                         if (type == GuestModeDialog.ENTERING) {
                             args = GUEST_BROWSING_ARG;
                         } else {
                             GeckoProfile.leaveGuestSession(BrowserApp.this);
+
+                            // Now's a good time to make sure we're not displaying the Guest Browsing notification.
+                            GuestSession.hideNotification(BrowserApp.this);
                         }
 
                         if (!GuestSession.isSecureKeyguardLocked(BrowserApp.this)) {
                             doRestart(args);
                         } else {
                             // If the secure keyguard is up, we don't want to restart.
                             // Just clear the guest profile data.
                             GeckoProfile.maybeCleanupGuestProfile(BrowserApp.this);
--- a/mobile/android/base/ChromeCast.java
+++ b/mobile/android/base/ChromeCast.java
@@ -188,17 +188,18 @@ class ChromeCast implements GeckoMediaPl
             public void onApplicationDisconnected(int errorCode) { }
         });
 
         apiClient = new GoogleApiClient.Builder(context)
             .addApi(Cast.API, apiOptionsBuilder.build())
             .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                 @Override
                 public void onConnected(Bundle connectionHint) {
-                    if (!apiClient.isConnected()) {
+                    // Sometimes apiClient is null here. See bug 1061032
+                    if (apiClient != null && !apiClient.isConnected()) {
                         debug("Connection failed");
                         callback.sendError("Not connected");
                         return;
                     }
 
                     // Launch the media player app and launch this url once its loaded
                     try {
                         Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
@@ -248,80 +249,95 @@ class ChromeCast implements GeckoMediaPl
         return true;
     }
 
     public void play(final EventCallback callback) {
         if (!verifySession(callback)) {
             return;
         }
 
-        remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
-            @Override
-            public void onResult(MediaChannelResult result) {
-                Status status = result.getStatus();
-                if (!status.isSuccess()) {
-                    debug("Unable to play: " + status.getStatusCode());
-                    callback.sendError(status.toString());
-                } else {
-                    callback.sendSuccess(null);
+        try {
+            remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
+                @Override
+                public void onResult(MediaChannelResult result) {
+                    Status status = result.getStatus();
+                    if (!status.isSuccess()) {
+                        debug("Unable to play: " + status.getStatusCode());
+                        callback.sendError(status.toString());
+                    } else {
+                        callback.sendSuccess(null);
+                    }
                 }
-            }
-        });
+            });
+        } catch(IllegalStateException ex) {
+            // The media player may throw if the session has been killed. For now, we're just catching this here.
+            callback.sendError("Error playing");
+        }
     }
 
     public void pause(final EventCallback callback) {
         if (!verifySession(callback)) {
             return;
         }
 
-        remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
-            @Override
-            public void onResult(MediaChannelResult result) {
-                Status status = result.getStatus();
-                if (!status.isSuccess()) {
-                    debug("Unable to pause: " + status.getStatusCode());
-                    callback.sendError(status.toString());
-                } else {
-                    callback.sendSuccess(null);
+        try {
+            remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
+                @Override
+                public void onResult(MediaChannelResult result) {
+                    Status status = result.getStatus();
+                    if (!status.isSuccess()) {
+                        debug("Unable to pause: " + status.getStatusCode());
+                        callback.sendError(status.toString());
+                    } else {
+                        callback.sendSuccess(null);
+                    }
                 }
-            }
-        });
+            });
+        } catch(IllegalStateException ex) {
+            // The media player may throw if the session has been killed. For now, we're just catching this here.
+            callback.sendError("Error pausing");
+        }
     }
 
     public void end(final EventCallback callback) {
         if (!verifySession(callback)) {
             return;
         }
 
-        Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
-            @Override
-            public void onResult(Status result) {
-                if (result.isSuccess()) {
-                    try {
-                        Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
-                        remoteMediaPlayer = null;
-                        mSessionId = null;
-                        apiClient.disconnect();
-                        apiClient = null;
+        try {
+            Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
+                @Override
+                public void onResult(Status result) {
+                    if (result.isSuccess()) {
+                        try {
+                            Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
+                            remoteMediaPlayer = null;
+                            mSessionId = null;
+                            apiClient.disconnect();
+                            apiClient = null;
 
-                        if (callback != null) {
-                            callback.sendSuccess(null);
-                        }
+                            if (callback != null) {
+                                callback.sendSuccess(null);
+                            }
 
-                        return;
-                    } catch(Exception ex) {
-                        debug("Error ending", ex);
+                            return;
+                        } catch(Exception ex) {
+                            debug("Error ending", ex);
+                        }
+                    }
+
+                    if (callback != null) {
+                        callback.sendError(result.getStatus().toString());
                     }
                 }
-
-                if (callback != null) {
-                    callback.sendError(result.getStatus().toString());
-                }
-            }
-        });
+            });
+        } catch(IllegalStateException ex) {
+            // The media player may throw if the session has been killed. For now, we're just catching this here.
+            callback.sendError("Error stopping");
+        }
     }
 
     class MirrorChannel implements MessageReceivedCallback {
         /**
          * @return custom namespace
          */
         public String getNamespace() {
             return "urn:x-cast:org.mozilla.mirror";
@@ -408,17 +424,18 @@ class ChromeCast implements GeckoMediaPl
                 public void onApplicationDisconnected(int errorCode) { }
             });
 
         apiClient = new GoogleApiClient.Builder(context)
             .addApi(Cast.API, apiOptionsBuilder.build())
             .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                     @Override
                     public void onConnected(Bundle connectionHint) {
-                        if (!apiClient.isConnected()) {
+                        // Sometimes apiClient is null here. See bug 1061032
+                        if (apiClient == null || !apiClient.isConnected()) {
                             return;
                         }
 
                         // Launch the media player app and launch this url once its loaded
                         try {
                             Cast.CastApi.launchApplication(apiClient, MIRROR_RECIEVER_APP_ID, true)
                                 .setResultCallback(new MirrorCallback(callback));
                         } catch (Exception e) {
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -4,17 +4,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.HttpURLConnection;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -40,17 +39,16 @@ import org.mozilla.gecko.health.SessionI
 import org.mozilla.gecko.health.StubbedHealthRecorder;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.menu.MenuPanel;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.PromptService;
-import org.mozilla.gecko.SmsManager;
 import org.mozilla.gecko.updater.UpdateService;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FileUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.NativeEventListener;
@@ -438,16 +436,19 @@ public abstract class GeckoApp
         }
 
         return super.onMenuOpened(featureId, menu);
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == R.id.quit) {
+            // Make sure the Guest Browsing notification goes away when we quit.
+            GuestSession.hideNotification(this);
+
             if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.GeckoRunning, GeckoThread.LaunchState.GeckoExiting)) {
                 final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
                 final Set<String> clearSet = PrefUtils.getStringSet(prefs, ClearOnShutdownPref.PREF, new HashSet<String>());
 
                 final JSONObject clearObj = new JSONObject();
                 for (String clear : clearSet) {
                     try {
                         clearObj.put(clear, true);
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -18,16 +18,17 @@ import java.util.Hashtable;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.INIParser;
 import org.mozilla.gecko.util.INISection;
 
+import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public final class GeckoProfile {
@@ -85,19 +86,29 @@ public final class GeckoProfile {
             // Check for a cached profile on this context already
             // TODO: We should not be caching profile information on the Activity context
             final GeckoApp geckoApp = (GeckoApp) context;
             if (geckoApp.mProfile != null) {
                 return geckoApp.mProfile;
             }
         }
 
-        // If the guest profile should be used return it.
-        if (GuestSession.shouldUse(context, "")) {
-            return GeckoProfile.getGuestProfile(context);
+        final String args;
+        if (context instanceof Activity) {
+            args = ((Activity) context).getIntent().getStringExtra("args");
+        } else {
+            args = null;
+        }
+
+        if (GuestSession.shouldUse(context, args)) {
+            GeckoProfile p = GeckoProfile.getOrCreateGuestProfile(context);
+            if (isGeckoApp) {
+                ((GeckoApp) context).mProfile = p;
+            }
+            return p;
         }
 
         if (isGeckoApp) {
             final GeckoApp geckoApp = (GeckoApp) context;
             String defaultProfileName;
             try {
                 defaultProfileName = geckoApp.getDefaultProfileName();
             } catch (NoMozillaDirectoryException e) {
@@ -186,16 +197,18 @@ public final class GeckoProfile {
             // Clear all shared prefs for the given profile.
             GeckoSharedPrefs.forProfileName(context, profileName)
                             .edit().clear().apply();
         }
 
         return success;
     }
 
+    // Only public for access from tests.
+    @RobocopTarget
     public static GeckoProfile createGuestProfile(Context context) {
         try {
             // We need to force the creation of a new guest profile if we want it outside of the normal profile path,
             // otherwise GeckoProfile.getDir will try to be smart and build it for us in the normal profiles dir.
             getGuestDir(context).mkdir();
             GeckoProfile profile = getGuestProfile(context);
 
             // If we're creating this guest session over the keyguard, don't lock it.
@@ -227,16 +240,28 @@ public final class GeckoProfile {
 
     private static File getGuestDir(Context context) {
         if (sGuestDir == null) {
             sGuestDir = context.getFileStreamPath("guest");
         }
         return sGuestDir;
     }
 
+    /**
+     * Performs IO. Be careful of using this on the main thread.
+     */
+    public static GeckoProfile getOrCreateGuestProfile(Context context) {
+        GeckoProfile p = getGuestProfile(context);
+        if (p == null) {
+            return createGuestProfile(context);
+        }
+
+        return p;
+    }
+
     public static GeckoProfile getGuestProfile(Context context) {
         if (sGuestProfile == null) {
             File guestDir = getGuestDir(context);
             if (guestDir.exists()) {
                 sGuestProfile = get(context, GUEST_PROFILE, guestDir);
                 sGuestProfile.mInGuestMode = true;
             }
         }
--- a/mobile/android/base/GuestSession.java
+++ b/mobile/android/base/GuestSession.java
@@ -1,37 +1,23 @@
 /* -*- 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;
 
+import android.app.KeyguardManager;
 import android.app.NotificationManager;
-import android.app.KeyguardManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.net.Uri;
-import android.util.Log;
+import android.support.v4.app.NotificationCompat;
 import android.view.Window;
 import android.view.WindowManager;
-import android.widget.ListView;
-import android.support.v4.app.NotificationCompat;
-
-import org.mozilla.gecko.prompts.Prompt;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import java.io.File;
-
-import org.json.JSONException;
-import org.json.JSONObject;
 
 // Utility methods for entering/exiting guest mode.
 public class GuestSession {
     public static final String NOTIFICATION_INTENT = "org.mozilla.gecko.GUEST_SESSION_INPROGRESS";
     private static final String LOGTAG = "GeckoGuestSession";
 
     // Returns true if the user is using a secure keyguard, and its currently locked.
     static boolean isSecureKeyguardLocked(Context context) {
@@ -100,19 +86,13 @@ public class GuestSession {
         manager.notify(R.id.guestNotification, builder.build());
     }
 
     public static void hideNotification(Context context) {
         final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         manager.cancel(R.id.guestNotification);
     }
 
-    public static void onDestroy(Context context) {
-        if (GeckoProfile.get(context).inGuestMode()) {
-            hideNotification(context);
-        }
-    }
-
     public static void handleIntent(BrowserApp context, Intent intent) {
         context.showGuestModeDialog(BrowserApp.GuestModeDialog.LEAVING);
     }
 
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -177,17 +177,17 @@
 <!ENTITY pref_donottrack_allow_tracking "Tell sites that I want to be tracked">
 <!ENTITY pref_donottrack_no_pref "Do not tell sites anything about my tracking preferences">
 
 <!ENTITY pref_char_encoding "Character encoding">
 <!ENTITY pref_char_encoding_on "Show menu">
 <!ENTITY pref_char_encoding_off "Don\'t show menu">
 <!ENTITY pref_clear_private_data2 "Clear now">
 <!ENTITY pref_clear_private_data_category "Clear private data">
-<!ENTITY pref_clear_on_exit_title "Always clear when quitting">
+<!ENTITY pref_clear_on_exit_title2 "Clear on exit">
 <!ENTITY pref_clear_on_exit_summary2 "&brandShortName; will automatically clear your data whenever you select \u0022Quit\u0022 from the main menu">
 <!ENTITY pref_clear_on_exit_dialog_title "Select which data to clear">
 <!ENTITY pref_plugins "Plugins">
 <!ENTITY pref_plugins_enabled "Enabled">
 <!ENTITY pref_plugins_tap_to_play "Tap to play">
 <!ENTITY pref_plugins_disabled "Disabled">
 <!ENTITY pref_text_size "Text size">
 <!ENTITY pref_reflow_on_zoom4 "Text reflow">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -186,17 +186,17 @@
   <string name="pref_donottrack_allow_tracking">&pref_donottrack_allow_tracking;</string>
   <string name="pref_donottrack_no_pref">&pref_donottrack_no_pref;</string>
 
   <string name="pref_char_encoding">&pref_char_encoding;</string>
   <string name="pref_char_encoding_on">&pref_char_encoding_on;</string>
   <string name="pref_char_encoding_off">&pref_char_encoding_off;</string>
   <string name="pref_clear_private_data">&pref_clear_private_data2;</string>
   <string name="pref_clear_private_data_category">&pref_clear_private_data_category;</string>
-  <string name="pref_clear_on_exit_title">&pref_clear_on_exit_title;</string>
+  <string name="pref_clear_on_exit_title">&pref_clear_on_exit_title2;</string>
   <string name="pref_clear_on_exit_summary2">&pref_clear_on_exit_summary2;</string>
   <string name="pref_clear_on_exit_dialog_title">&pref_clear_on_exit_dialog_title;</string>
   <string name="pref_plugins">&pref_plugins;</string>
   <string name="pref_plugins_enabled">&pref_plugins_enabled;</string>
   <string name="pref_plugins_tap_to_play">&pref_plugins_tap_to_play;</string>
   <string name="pref_plugins_disabled">&pref_plugins_disabled;</string>
   <string name="pref_text_size">&pref_text_size;</string>
   <string name="pref_font_size_tiny">&pref_font_size_tiny;</string>
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -50,43 +50,43 @@ class TabsGridLayout extends GridView
 
         mTabsAdapter = new TabsGridLayoutAdapter(mContext);
         setAdapter(mTabsAdapter);
 
         setRecyclerListener(new RecyclerListener() {
             @Override
             public void onMovedToScrapHeap(View view) {
                 TabsLayoutItemView item = (TabsLayoutItemView) view;
-                item.thumbnail.setImageDrawable(null);
+                item.setThumbnail(null);
             }
         });
     }
 
     private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
 
         final private Button.OnClickListener mCloseClickListener;
         final private View.OnClickListener mSelectClickListener;
 
         public TabsGridLayoutAdapter (Context context) {
             super(context);
 
             mCloseClickListener = new Button.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
-                    Tab tab = Tabs.getInstance().getTab(itemView.id);
+                    Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
                     Tabs.getInstance().closeTab(tab);
                 }
             };
 
             mSelectClickListener = new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     TabsLayoutItemView tab = (TabsLayoutItemView) v;
-                    Tabs.getInstance().selectTab(tab.id);
+                    Tabs.getInstance().selectTab(tab.getTabId());
                     autoHidePanel();
                 }
             };
         }
 
         @Override
         TabsLayoutItemView newView(int position, ViewGroup parent) {
             final TabsLayoutItemView item = super.newView(position, parent);
--- a/mobile/android/base/tabs/TabsLayoutItemView.java
+++ b/mobile/android/base/tabs/TabsLayoutItemView.java
@@ -19,23 +19,21 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class TabsLayoutItemView extends LinearLayout
                                 implements Checkable {
     private static final String LOGTAG = "Gecko" + TabsLayoutItemView.class.getSimpleName();
     private static final int[] STATE_CHECKED = { android.R.attr.state_checked };
     private boolean mChecked;
 
-    // yeah, it's a bit nasty having two different styles for the class members,
-    // this'll be fixed once bug 1058574 is addressed
-    int id;
-    TextView title;
-    ImageView thumbnail;
-    ImageButton close;
-    TabThumbnailWrapper thumbnailWrapper;
+    private int mTabId;
+    private TextView mTitle;
+    private ImageView mThumbnail;
+    private ImageButton mCloseButton;
+    private TabThumbnailWrapper mThumbnailWrapper;
 
     public TabsLayoutItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
     public int[] onCreateDrawableState(int extraSpace) {
         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
@@ -71,40 +69,52 @@ public class TabsLayoutItemView extends 
     }
 
     @Override
     public void toggle() {
         mChecked = !mChecked;
     }
 
     public void setCloseOnClickListener(OnClickListener mOnClickListener) {
-        close.setOnClickListener(mOnClickListener);
+        mCloseButton.setOnClickListener(mOnClickListener);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        title = (TextView) findViewById(R.id.title);
-        thumbnail = (ImageView) findViewById(R.id.thumbnail);
-        close = (ImageButton) findViewById(R.id.close);
-        thumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
+        mTitle = (TextView) findViewById(R.id.title);
+        mThumbnail = (ImageView) findViewById(R.id.thumbnail);
+        mCloseButton = (ImageButton) findViewById(R.id.close);
+        mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
     }
 
     protected void assignValues(Tab tab)  {
         if (tab == null) {
             return;
         }
 
-        id = tab.getId();
+        mTabId = tab.getId();
 
         Drawable thumbnailImage = tab.getThumbnail();
         if (thumbnailImage != null) {
-            thumbnail.setImageDrawable(thumbnailImage);
+            mThumbnail.setImageDrawable(thumbnailImage);
         } else {
-            thumbnail.setImageResource(R.drawable.tab_thumbnail_default);
+            mThumbnail.setImageResource(R.drawable.tab_thumbnail_default);
+        }
+        if (mThumbnailWrapper != null) {
+            mThumbnailWrapper.setRecording(tab.isRecording());
         }
-        if (thumbnailWrapper != null) {
-            thumbnailWrapper.setRecording(tab.isRecording());
-        }
-        title.setText(tab.getDisplayTitle());
-        close.setTag(this);
+        mTitle.setText(tab.getDisplayTitle());
+        mCloseButton.setTag(this);
+    }
+
+    public int getTabId() {
+        return mTabId;
+    }
+
+    public void setThumbnail(Drawable thumbnail) {
+        mThumbnail.setImageDrawable(thumbnail);
+    }
+
+    public void setCloseVisibile(boolean visible) {
+        mCloseButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
     }
 }
--- a/mobile/android/base/tabs/TabsListLayout.java
+++ b/mobile/android/base/tabs/TabsListLayout.java
@@ -75,18 +75,18 @@ class TabsListLayout extends TwoWayView
         mSwipeListener = new TabSwipeGestureListener();
         setOnTouchListener(mSwipeListener);
         setOnScrollListener(mSwipeListener.makeScrollListener());
 
         setRecyclerListener(new RecyclerListener() {
             @Override
             public void onMovedToScrapHeap(View view) {
                 TabsLayoutItemView item = (TabsLayoutItemView) view;
-                item.thumbnail.setImageDrawable(null);
-                item.close.setVisibility(View.VISIBLE);
+                item.setThumbnail(null);
+                item.setCloseVisibile(true);
             }
         });
     }
 
     private class TabsListLayoutAdapter extends TabsLayoutAdapter {
         private Button.OnClickListener mCloseOnClickListener;
         public TabsListLayoutAdapter (Context context) {
             super(context);
@@ -355,17 +355,17 @@ class TabsListLayout extends TwoWayView
         PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
 
         final boolean isVertical = isVertical();
         if (isVertical)
             animator.attach(view, Property.HEIGHT, 1);
         else
             animator.attach(view, Property.WIDTH, 1);
 
-        final int tabId = ((TabsLayoutItemView) view).id;
+        final int tabId = ((TabsLayoutItemView) view).getTabId();
 
         // Caching this assumes that all rows are the same height
         if (mOriginalSize == 0) {
             mOriginalSize = (isVertical ? view.getHeight() : view.getWidth());
         }
 
         animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
             @Override
@@ -392,17 +392,17 @@ class TabsListLayout extends TwoWayView
 
 
         animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
             @Override
             public void onPropertyAnimationStart() { }
             @Override
             public void onPropertyAnimationEnd() {
                 TabsLayoutItemView tab = (TabsLayoutItemView) view;
-                tab.close.setVisibility(View.VISIBLE);
+                tab.setCloseVisibile(true);
             }
         });
 
         animator.start();
     }
 
     private class TabSwipeGestureListener implements View.OnTouchListener {
         // same value the stock browser uses for after drag animation velocity in pixels/sec
@@ -490,17 +490,17 @@ class TabsListLayout extends TwoWayView
                     if (mSwipeView == null)
                         break;
 
                     cancelCheckForTap();
                     mSwipeView.setPressed(false);
 
                     if (!mSwiping) {
                         TabsLayoutItemView item = (TabsLayoutItemView) mSwipeView;
-                        Tabs.getInstance().selectTab(item.id);
+                        Tabs.getInstance().selectTab(item.getTabId());
                         autoHidePanel();
 
                         mVelocityTracker.recycle();
                         mVelocityTracker = null;
                         break;
                     }
 
                     mVelocityTracker.addMovement(e);
@@ -577,17 +577,17 @@ class TabsListLayout extends TwoWayView
                     // set pressed state on the swiped view.
                     if (isScrollingX || isScrollingY)
                         cancelCheckForTap();
 
                     if (isSwipingToClose) {
                         mSwiping = true;
                         TabsListLayout.this.requestDisallowInterceptTouchEvent(true);
 
-                        ((TabsLayoutItemView) mSwipeView).close.setVisibility(View.INVISIBLE);
+                        ((TabsLayoutItemView) mSwipeView).setCloseVisibile(false);
 
                         // Stops listview from highlighting the touched item
                         // in the list when swiping.
                         MotionEvent cancelEvent = MotionEvent.obtain(e);
                         cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
                                 (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                         TabsListLayout.this.onTouchEvent(cancelEvent);
                         cancelEvent.recycle();
--- a/mobile/android/components/HelperAppDialog.js
+++ b/mobile/android/components/HelperAppDialog.js
@@ -114,17 +114,17 @@ HelperAppLauncherDialog.prototype = {
       if (!win) {
         // Oops.
         Services.console.logStringMessage("Refusing download, but can't show a toast.");
         return;
       }
 
       Services.console.logStringMessage("Refusing download of non-downloadable file.");
       let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
-      let failedText = bundle.GetStringFromName("protocol.failed");
+      let failedText = bundle.GetStringFromName("download.blocked");
       win.toast.show(failedText, "long");
 
       return;
     }
 
     let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
 
     let defaultHandler = new Object();
--- a/mobile/android/locales/en-US/chrome/handling.properties
+++ b/mobile/android/locales/en-US/chrome/handling.properties
@@ -1,7 +1,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/.
 
+download.blocked=Unable to download file
 protocol.failed=Couldn't find an app to open this link
 # A very short string shown in the button toast when no application can open the url
 protocol.toast.search=Search
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -137,19 +137,20 @@ class XPCShellRunner(MozbuildObject):
 
         xpcshell_filter = TestStartFilter()
         self.log_manager.terminal_handler.addFilter(xpcshell_filter)
 
         tests_dir = os.path.join(self.topobjdir, '_tests', 'xpcshell')
         modules_dir = os.path.join(self.topobjdir, '_tests', 'modules')
         # We want output from the test to be written immediately if we are only
         # running a single test.
-        verbose_output = (test_path is not None or
-                          (manifest and len(manifest.test_paths())==1) or
-                          verbose)
+        single_test = (test_path is not None or
+                       (manifest and len(manifest.test_paths())==1))
+        verbose_output = verbose or single_test
+        sequential = sequential or single_test
 
         args = {
             'manifest': manifest,
             'xpcshell': self.get_binary_path('xpcshell'),
             'mozInfo': os.path.join(self.topobjdir, 'mozinfo.json'),
             'symbolsPath': os.path.join(self.distdir, 'crashreporter-symbols'),
             'interactive': interactive,
             'keepGoing': keep_going,
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -2,65 +2,67 @@
  * 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/. */
 
 "use strict";
 
 /**
  * This module provides an asynchronous API for managing bookmarks.
  *
- * Bookmarks are organized in a tree structure, and can be bookmarked URIs,
- * folders or separators.  Multiple bookmarks for the same URI are allowed.
+ * Bookmarks are organized in a tree structure, and can be bookmarked URLs,
+ * folders or separators.  Multiple bookmarks for the same URL are allowed.
  *
  * Note that if you are handling bookmarks operations in the UI, you should
  * not use this API directly, but rather use PlacesTransactions.jsm, so that
  * any operation is undo/redo-able.
  *
  * Each bookmarked item is represented by an object having the following
  * properties:
  *
  *  - guid (string)
  *      The globally unique identifier of the item.
  *  - parentGuid (string)
  *      The globally unique identifier of the folder containing the item.
  *      This will be an empty string for the Places root folder.
  *  - index (number)
  *      The 0-based position of the item in the parent folder.
- *  - dateAdded (number, microseconds from the epoch)
- *      The time at which the item was added.  This is a PRTime (microseconds).
- *  - lastModified (number, microseconds from the epoch)
- *      The time at which the item was last modified. This is a PRTime (microseconds).
+ *  - dateAdded (Date)
+ *      The time at which the item was added.
+ *  - lastModified (Date)
+ *      The time at which the item was last modified.
  *  - type (number)
  *      The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
  *
  *  The following properties are only valid for bookmarks or folders.
  *
  *  - title (string)
  *      The item's title, if any.  Empty titles and null titles are considered
  *      the same and the property is unset on retrieval in such a case.
+ *      Title cannot be longer than TITLE_LENGTH_MAX, or it will be truncated.
  *
  *  The following properties are only valid for bookmarks:
  *
- *  - uri (nsIURI)
- *      The item's URI.
+ *  - url (URL, href or nsIURI)
+ *      The item's URL.  Note that while input objects can contains either
+ *      an URL object, an href string, or an nsIURI, output objects will always
+ *      contain an URL object.
+ *      URL cannot be longer than URL_LENGTH_MAX, methods will throw if a
+ *      longer value is provided
  *  - keyword (string)
  *      The associated keyword, if any.
  *
  * Each successful operation notifies through the nsINavBookmarksObserver
  * interface.  To listen to such notifications you must register using
  * nsINavBookmarksService addObserver and removeObserver methods.
  * Note that bookmark addition or order changes won't notify onItemMoved for
  * items that have their indexes changed.
  * Similarly, lastModified changes not done explicitly (like changing another
  * property) won't fire an onItemChanged notification for the lastModified
  * property.
  * @see nsINavBookmarkObserver
- *
- * @note livemarks are implemented as empty folders.
- *       @see mozIAsyncLivemarks.idl
  */
 
 this.EXPORTED_SYMBOLS = [ "Bookmarks" ];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
@@ -69,175 +71,189 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
 
-const URI_LENGTH_MAX = 65536;
+const URL_LENGTH_MAX = 65536;
 const TITLE_LENGTH_MAX = 4096;
 
 let Bookmarks = Object.freeze({
   /**
    * Item's type constants.
    * These should stay consistent with nsINavBookmarksService.idl
    */
   TYPE_BOOKMARK: 1,
   TYPE_FOLDER: 2,
   TYPE_SEPARATOR: 3,
 
   /**
+   * Default index used to append a bookmarked item at the end of a container. 
+   * This should stay consistent with nsINavBookmarksService.idl
+   */
+  DEFAULT_INDEX: -1,
+
+  /**
    * Creates or updates a bookmarked item.
    *
    * If the given guid is found the corresponding item is updated, otherwise,
    * if no guid is provided, a bookmark is created and a new guid is assigned
    * to it.
    *
    * In the creation case, a minimum set of properties must be provided:
    *  - type
    *  - parentGuid
-   *  - URI, only for bookmarks
+   *  - url, only for bookmarks
    * If an index is not specified, it defaults to appending.
    * It's also possible to pass a non-existent guid to force creation of an
    * item with the given guid, but unless you have a very sound reason, such as
    * an undo manager implementation or synchronization, you should not do that.
    *
    * In the update case, you should only set the properties which should be
    * changed, undefined properties won't be taken into account for the update.
    * Moreover, the item's type and the guid are ignored, since they are
    * immutable after creation.  Note that if the passed in values are not
    * coherent with the known values, this rejects.
    * Passing null or an empty string as keyword clears any keyword
    * associated with this bookmark.
    *
    * Note that any known property that doesn't apply to the specific item type
-   * causes rejection.
+   * throws an exception.
    *
    * @param info
    *        object representing a bookmarked item, as defined above.
+   * @param onResult [optional]
+   *        Callback invoked for each created or updated bookmark.  It gets
+   *        the bookmark object as argument.
    *
    * @return {Promise} resolved when the update is complete.
-   * @resolves to the input object, updated with relevant information.
-   * @rejects JavaScript exception.
-   *
-   * @note title is truncated to TITLE_LENGTH_MAX and URI is rejected if
-   *       greater than URI_LENGTH_MAX.
+   * @resolves to a boolean indicating whether any new bookmark has been created.
+   * @rejects JavaScript exception if it's not possible to update or create the
+   *          requested bookmark.
+   * @throws if input is invalid.
    */
   // XXX WIP XXX Will replace functionality from these methods:
   // long long insertBookmark(in long long aParentId, in nsIURI aURI, in long aIndex, in AUTF8String aTitle, [optional] in ACString aGUID);
   // long long createFolder(in long long aParentFolder, in AUTF8String name, in long index, [optional] in ACString aGUID);
   // void moveItem(in long long aItemId, in long long aNewParentId, in long aIndex);
   // long long insertSeparator(in long long aParentId, in long aIndex, [optional] in ACString aGUID);
   // void setItemTitle(in long long aItemId, in AUTF8String aTitle);
   // void setItemDateAdded(in long long aItemId, in PRTime aDateAdded);
   // void setItemLastModified(in long long aItemId, in PRTime aLastModified);
   // void changeBookmarkURI(in long long aItemId, in nsIURI aNewURI);
   // void setKeywordForBookmark(in long long aItemId, in AString aKeyword);
-  update: Task.async(function* (info) {
+  update: function (info, onResult=null) {
     throw new Error("Not yet implemented");
-  }),
+  },
 
   /**
    * Removes a bookmarked item.
    *
    * Input can either be a guid or an object with one of the following
    * properties set:
    *  - guid: if set, only the corresponding item is removed.
    *  - parentGuid: if it's set and is a folder, any children of that folder is
    *                removed, but not the folder itself.
-   *  - URI: if set, any bookmark for that URI is removed.
-   * If multiple of these properties are set, the method rejects.
+   *  - url: if set, any bookmark for that URL is removed.
+   * If multiple of these properties are set, the method throws.
    *
    * Any other property is ignored, known properties may be overwritten.
    *
    * @param guidOrInfo
    *        The globally unique identifier of the item to remove, or an
    *        object representing it, as defined above.
+   * @param onResult [optional]
+   *        Callback invoked for each removed bookmark.  It gets the bookmark
+   *        object as argument.
    *
    * @return {Promise} resolved when the removal is complete.
-   * @resolves to the removed object or an array of them.
-   * @rejects JavaScript exception.
+   * @resolves to a boolean indicating whether any object has been removed.
+   * @rejects JavaScript exception if the provided guid or parentGuid don't
+   *          match any existing bookmark.
+   * @throws if input is invalid.
    */
   // XXX WIP XXX Will replace functionality from these methods:
   // removeItem(in long long aItemId);
   // removeFolderChildren(in long long aItemId);
-  remove: Task.async(function* (guidOrInfo) {
+  remove: function (guidOrInfo, onResult=null) {
     throw new Error("Not yet implemented");
-  }),
+  },
 
   /**
    * Fetches information about a bookmarked item.
    *
    * Input can be either a guid or an object with one, and only one, of these
    * filtering properties set:
    *  - guid
    *      retrieves the item with the specified guid
    *  - parentGuid and index
    *      retrieves the item by its position
-   *  - URI
-   *      retrieves all items having the given URI.
+   *  - url
+   *      retrieves an array of items having the given URL.
    *  - keyword
-   *      retrieves all items having the given keyword.
+   *      retrieves an array of items having the given keyword.
    *
    * Any other property is ignored.  Known properties may be overwritten.
    *
    * @param guidOrInfo
    *        The globally unique identifier of the item to fetch, or an
    *        object representing it, as defined above.
    *
    * @return {Promise} resolved when the fetch is complete.
    * @resolves to an object representing the found item, as described above, or
    *           an array of such objects.  if no item is found, the returned
    *           promise is resolved to null.
    * @rejects JavaScript exception.
+   * @throws if input is invalid.
    */
   // XXX WIP XXX Will replace functionality from these methods:
   // long long getIdForItemAt(in long long aParentId, in long aIndex);
   // AUTF8String getItemTitle(in long long aItemId);
   // PRTime getItemDateAdded(in long long aItemId);
   // PRTime getItemLastModified(in long long aItemId);
   // nsIURI getBookmarkURI(in long long aItemId);
   // long getItemIndex(in long long aItemId);
   // unsigned short getItemType(in long long aItemId);
   // boolean isBookmarked(in nsIURI aURI);
   // long long getFolderIdForItem(in long long aItemId);
   // void getBookmarkIdsForURI(in nsIURI aURI, [optional] out unsigned long count, [array, retval, size_is(count)] out long long bookmarks);
   // AString getKeywordForURI(in nsIURI aURI);
   // AString getKeywordForBookmark(in long long aItemId);
   // nsIURI getURIForKeyword(in AString keyword);
-  fetch: Task.async(function* (guidOrInfo) {
+  fetch: function (guidOrInfo) {
     throw new Error("Not yet implemented");
-  }),
+  },
 
   /**
    * Retrieves an object representation of a bookmarked item, along with all of
    * its descendants, if any.
    *
-   * Each node in the tree is an object that extends
-   * the item representation described above with some additional properties:
+   * Each node in the tree is an object that extends the item representation
+   * described above with some additional properties:
    *
    *  - [deprecated] id (number)
    *      the item's id.  Defined only if aOptions.includeItemIds is set.
    *  - annos (array)
    *      the item's annotations.  This is not set if there are no annotations
    *      set for the item.
    *
    * The root object of the tree also has the following properties set:
    *  - itemsCount (number, not enumerable)
    *      the number of items, including the root item itself, which are
    *      represented in the resolved object.
    *
-   * Bookmarked URIs may also have the following properties:
+   * Bookmarked URLs may also have the following properties:
    *  - tags (string)
    *      csv string of the bookmark's tags, if any.
    *  - charset (string)
    *      the last known charset of the bookmark, if any.
-   *  - iconuri (string)
+   *  - iconurl (URL)
    *      the bookmark's favicon URL, if any.
    *
    * Folders may also have the following properties:
    *  - children (array)
    *      the folder's children information, each of them having the same set of
    *      properties as above.
    *
    * @param [optional] guid
@@ -261,35 +277,37 @@ let Bookmarks = Object.freeze({
    *           Use it if you must. It'll be removed once the switch to guids is
    *           complete.
    *
    * @return {Promise} resolved when the fetch is complete.
    * @resolves to an object that represents either a single item or a
    *           bookmarks tree.  if guid points to a non-existent item, the
    *           returned promise is resolved to null.
    * @rejects JavaScript exception.
+   * @throws if input is invalid.
    */
   // XXX WIP XXX: will replace functionality for these methods:
   // PlacesUtils.promiseBookmarksTree()
-  fetchTree: Task.async(function* (guid = "", options = {}) {
+  fetchTree: function (guid = "", options = {}) {
     throw new Error("Not yet implemented");
-  }),
+  },
 
   /**
    * Reorders contents of a folder based on a provided array of GUIDs.
    *
    * @param parentGuid
    *        The globally unique identifier of the folder whose contents should
    *        be reordered.
    * @param orderedChildrenGuids
    *        Ordered array of the children's GUIDs.  If this list contains
    *        non-existing entries they will be ignored.  If the list is
    *        incomplete, missing entries will be appended.
    *
    * @return {Promise} resolved when reordering is complete.
    * @rejects JavaScript exception.
+   * @throws if input is invalid.
    */
   // XXX WIP XXX Will replace functionality from these methods:
   // void setItemIndex(in long long aItemId, in long aNewIndex);
-  reorder: Task.async(function* (parentGuid, orderedChildrenGuids) {
+  reorder: function (parentGuid, orderedChildrenGuids) {
     throw new Error("Not yet implemented");
-  })
+  }
 });
--- a/toolkit/themes/linux/global/autocomplete.css
+++ b/toolkit/themes/linux/global/autocomplete.css
@@ -112,33 +112,36 @@ treechildren.autocomplete-treebody::-moz
   color: HighlightText;
 }
 
 .autocomplete-richlistitem {
   padding: 6px 2px;
   color: MenuText;
 }
 
+.ac-url-box {
+  /* When setting a vertical margin here, half of that needs to be added
+     .ac-title-box's translateY for when .ac-url-box is hidden (see below). */
+  margin-top: 1px;
+}
+
 .autocomplete-richlistitem[actiontype="keyword"] .ac-url-box,
 .autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box,
 .autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box,
 .autocomplete-richlistitem[type~="autofill"] .ac-url-box {
-  display: none;
+  visibility: hidden;
 }
 
 .autocomplete-richlistitem[actiontype="keyword"] .ac-title-box,
 .autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box,
 .autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box,
 .autocomplete-richlistitem[type~="autofill"] .ac-title-box {
-  margin-top: 12px;
-  margin-bottom: 12px;
-}
-
-.ac-url-box {
-  margin-top: 1px;
+  /* Center the title by moving it down by half of .ac-url-box's height,
+     including vertical margins (if any). */
+  transform: translateY(.5em);
 }
 
 .ac-site-icon {
   width: 16px; 
   height: 16px;
   margin-bottom: -2px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
--- a/toolkit/themes/osx/global/autocomplete.css
+++ b/toolkit/themes/osx/global/autocomplete.css
@@ -97,33 +97,36 @@ treechildren.autocomplete-treebody::-moz
   color: HighlightText;
   background-image: linear-gradient(rgba(255,255,255,0.3), transparent);
 }
 
 .autocomplete-richlistitem {
   padding: 5px 2px;
 }
 
+.ac-url-box {
+  /* When setting a vertical margin here, half of that needs to be added
+     .ac-title-box's translateY for when .ac-url-box is hidden (see below). */
+  margin-top: 1px;
+}
+
 .autocomplete-richlistitem[actiontype="keyword"] .ac-url-box,
 .autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box,
 .autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box,
 .autocomplete-richlistitem[type~="autofill"] .ac-url-box {
-  display: none;
+  visibility: hidden;
 }
 
 .autocomplete-richlistitem[actiontype="keyword"] .ac-title-box,
 .autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box,
 .autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box,
 .autocomplete-richlistitem[type~="autofill"] .ac-title-box {
-  margin-top: 12px;
-  margin-bottom: 12px;
-}
-
-.ac-url-box {
-  margin-top: 1px;
+  /* Center the title by moving it down by half of .ac-url-box's height,
+     including vertical margins (if any). */
+  transform: translateY(.5em);
 }
 
 .ac-site-icon {
   width: 16px; 
   height: 16px;
   margin-bottom: -1px;
   -moz-margin-start: 7px;
   -moz-margin-end: 5px;
--- a/toolkit/themes/windows/global/autocomplete.css
+++ b/toolkit/themes/windows/global/autocomplete.css
@@ -125,37 +125,40 @@ treechildren.autocomplete-treebody::-moz
     border-radius: 6px;
     outline: 1px solid rgb(124,163,206);
     -moz-outline-radius: 3px;
     outline-offset: -2px;
   }
 }
 %endif
 
+.ac-title-box {
+  margin-top: 4px;
+}
+
+.ac-url-box {
+  /* When setting a vertical margin here, half of that needs to be added
+     .ac-title-box's translateY for when .ac-url-box is hidden (see below). */
+  margin: 1px 0 4px;
+}
+
 .autocomplete-richlistitem[actiontype="keyword"] .ac-url-box,
 .autocomplete-richlistitem[actiontype="searchengine"] .ac-url-box,
 .autocomplete-richlistitem[actiontype="visiturl"] .ac-url-box,
 .autocomplete-richlistitem[type~="autofill"] .ac-url-box {
-  display: none;
+  visibility: hidden;
 }
 
 .autocomplete-richlistitem[actiontype="keyword"] .ac-title-box,
 .autocomplete-richlistitem[actiontype="searchengine"] .ac-title-box,
 .autocomplete-richlistitem[actiontype="visiturl"] .ac-title-box,
 .autocomplete-richlistitem[type~="autofill"] .ac-title-box {
-  margin-top: 12px;
-  margin-bottom: 12px;
-}
-
-.ac-title-box {
-  margin-top: 4px;
-}
-
-.ac-url-box {
-  margin: 1px 0 4px;
+  /* Center the title by moving it down by half of .ac-url-box's height,
+     including vertical margins (if any). */
+  transform: translateY(calc(.5em + 2px));
 }
 
 .ac-site-icon {
   width: 16px; 
   height: 16px;
   margin: 0 5px -2px;
 }