merge b2g-inbound to mozilla-central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 29 Nov 2013 10:19:20 +0100
changeset 172763 1e3712793e7d09e683ecc2f951a46926c854caf2
parent 172707 0e6b2fa40378f302fc95467dd444d06616939d5d (current diff)
parent 172762 d27d0489b68998bfd9434a22c434ef2a07a51f93 (diff)
child 172764 e9337081c744f089be3950818b8e5b9d563a0298
child 172779 0bd264d20f3fecb416abb25f9567e23b2140a30f
child 172843 c1b184d7050c0f1bc119a7b2582fe1148897b8d3
child 172882 38785c275471a7895bf952a406c3b9d6cf78a7d6
push id3224
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:06:49 +0000
treeherdermozilla-beta@60c04d0987f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone28.0a1
first release with
nightly linux32
1e3712793e7d / 28.0a1 / 20131129030200 / files
nightly linux64
1e3712793e7d / 28.0a1 / 20131129030200 / files
nightly mac
1e3712793e7d / 28.0a1 / 20131129030200 / files
nightly win64
1e3712793e7d / 28.0a1 / 20131129030200 / files
nightly win32
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win64
merge b2g-inbound to mozilla-central
b2g/installer/package-manifest.in
dom/base/Navigator.cpp
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -833,8 +833,12 @@ pref("gfx.gralloc.fence-with-readpixels"
 // Cell Broadcast API
 pref("ril.cellbroadcast.disabled", false);
 
 // The url of the page used to display network error details.
 pref("b2g.neterror.url", "app://system.gaiamobile.org/net_error.html");
 
 // Enable Web Speech synthesis API
 pref("media.webspeech.synth.enabled", true);
+
+// Downloads API
+pref("dom.mozDownloads.enabled", true);
+pref("dom.downloads.max_retention_days", 7);
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -21,16 +21,18 @@ Cu.import('resource://gre/modules/ErrorP
 #ifdef MOZ_WIDGET_GONK
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
 #endif
 
 // identity
 Cu.import('resource://gre/modules/SignInToWebsite.jsm');
 SignInToWebsiteController.init();
 
+Cu.import('resource://gre/modules/DownloadsAPI.jsm');
+
 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
                                    '@mozilla.org/process/environment;1',
                                    'nsIEnvironment');
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'ss',
                                    '@mozilla.org/content/style-sheet-service;1',
                                    'nsIStyleSheetService');
 
@@ -1459,8 +1461,26 @@ Services.obs.addObserver(function resetP
 #endif
   },
   'profile-before-change2', false);
 
   let appStartup = Cc['@mozilla.org/toolkit/app-startup;1']
                      .getService(Ci.nsIAppStartup);
   appStartup.quit(Ci.nsIAppStartup.eForceQuit);
 }, 'b2g-reset-profile', false);
+
+/**
+  * CID of our implementation of nsIDownloadManagerUI.
+  */
+const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
+
+/**
+  * Contract ID of the service implementing nsITransfer.
+  */
+const kTransferContractId = "@mozilla.org/transfer;1";
+
+// Override Toolkit's nsITransfer implementation with the one from the
+// JavaScript API for downloads.  This will eventually be removed when
+// nsIDownloadManager will not be available anymore (bug 851471).  The
+// old code in this module will be removed in bug 899110.
+Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
+                  .registerFactory(kTransferCid, "",
+                                   kTransferContractId, null);
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "3fe38577e17209728a54b14624143dbf99ade2df", 
+    "revision": "121f70034b7ef01836aa345f8ff37b61b96ac88e", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/confvars.sh
+++ b/b2g/confvars.sh
@@ -53,8 +53,10 @@ MOZ_TIME_MANAGER=1
 MOZ_B2G_CERTDATA=1
 MOZ_PAY=1
 MOZ_TOOLKIT_SEARCH=
 MOZ_PLACES=
 MOZ_B2G=1
 
 #MOZ_NUWA_PROCESS=1
 MOZ_FOLD_LIBS=1
+
+MOZ_JSDOWNLOADS=1
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -388,16 +388,18 @@
 @BINPATH@/components/SiteSpecificUserAgent.js
 @BINPATH@/components/SiteSpecificUserAgent.manifest
 @BINPATH@/components/storage-mozStorage.js
 @BINPATH@/components/crypto-SDR.js
 @BINPATH@/components/jsconsole-clhandler.manifest
 @BINPATH@/components/jsconsole-clhandler.js
 @BINPATH@/components/nsDownloadManagerUI.manifest
 @BINPATH@/components/nsDownloadManagerUI.js
+@BINPATH@/components/Downloads.manifest
+@BINPATH@/components/DownloadLegacy.js
 @BINPATH@/components/nsSidebar.manifest
 @BINPATH@/components/nsSidebar.js
 
 ; WiFi, NetworkManager, NetworkStats
 #ifdef MOZ_WIDGET_GONK
 @BINPATH@/components/DOMWifiManager.js
 @BINPATH@/components/DOMWifiManager.manifest
 @BINPATH@/components/NetworkInterfaceListService.js
@@ -557,16 +559,19 @@
 @BINPATH@/components/TCPSocketParentIntermediary.js
 @BINPATH@/components/TCPSocket.manifest
 
 @BINPATH@/components/Payment.js
 @BINPATH@/components/PaymentFlowInfo.js
 @BINPATH@/components/PaymentRequestInfo.js
 @BINPATH@/components/Payment.manifest
 
+@BINPATH@/components/DownloadsAPI.js
+@BINPATH@/components/DownloadsAPI.manifest
+
 ; InputMethod API
 @BINPATH@/components/MozKeyboard.js
 @BINPATH@/components/InputMethod.manifest
 
 ; Modules
 @BINPATH@/modules/*
 
 ; Safe Browsing
@@ -780,16 +785,17 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/YoutubeProtocolHandler.js
 @BINPATH@/components/RecoveryService.js
 @BINPATH@/components/MailtoProtocolHandler.js
 @BINPATH@/components/SmsProtocolHandler.js
 @BINPATH@/components/TelProtocolHandler.js
 @BINPATH@/components/B2GAboutRedirector.js
 @BINPATH@/components/FilePicker.js
 @BINPATH@/components/HelperAppDialog.js
+@BINPATH@/components/DownloadsUI.js
 
 @BINPATH@/components/DataStore.manifest
 @BINPATH@/components/DataStoreService.js
 @BINPATH@/components/dom_datastore.xpt
 
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechsynth.xpt
 #endif
--- a/content/events/test/test_all_synthetic_events.html
+++ b/content/events/test/test_all_synthetic_events.html
@@ -123,16 +123,20 @@ const kEventConstructors = {
   DeviceProximityEvent:                      { create: function (aName, aProps) {
                                                          return new DeviceProximityEvent(aName, aProps);
                                                        },
                                              },
   DeviceStorageChangeEvent:                  { create: function (aName, aProps) {
                                                          return new DeviceStorageChangeEvent(aName, aProps);
                                                        },
                                              },
+  DownloadEvent:                             { create: function (aName, aProps) {
+                                                         return new DownloadEvent(aName, aProps);
+                                                       },
+                                             },
   DOMTransactionEvent:                       { create: function (aName, aProps) {
                                                          return new DOMTransactionEvent(aName, aProps);
                                                        },
                                              },
   DragEvent:                                 { create: function (aName, aProps) {
                                                          var e = document.createEvent("dragevent");
                                                          e.initDragEvent(aName, aProps.bubbles, aProps.cancelable,
                                                                          aProps.view, aProps.detail,
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -313,16 +313,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "speaker-control": {
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
+                           "downloads": {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
                          };
 
 /**
  * Append access modes to the permission name as suffixes.
  *   e.g. permission name 'contacts' with ['read', 'write'] =
  *   ['contacts-read', contacts-write']
  * @param string aPermName
  * @param array aAccess
--- a/dom/base/DOMRequestHelper.jsm
+++ b/dom/base/DOMRequestHelper.jsm
@@ -180,22 +180,23 @@ DOMRequestIpcHelper.prototype = {
         this._listeners[aName] ? cpmm.removeMessageListener(aName, this)
                                : cpmm.removeWeakMessageListener(aName, this);
         delete this._listeners[aName];
       });
     }
 
     this._listeners = null;
     this._requests = null;
-    this._window = null;
 
     // Objects inheriting from DOMRequestIPCHelper may have an uninit function.
     if (this.uninit) {
       this.uninit();
     }
+
+    this._window = null;
   },
 
   observe: function(aSubject, aTopic, aData) {
     if (aTopic !== "inner-window-destroyed") {
       return;
     }
 
     let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1531,16 +1531,23 @@ Navigator::DoNewResolve(JSContext* aCx, 
         bool hasPermission = CheckPermission("settings-read") ||
                              CheckPermission("settings-write");
         if (!hasPermission) {
           aValue.setNull();
           return true;
         }
       }
 
+      if (name.EqualsLiteral("mozDownloadManager")) {
+        if (!CheckPermission("downloads")) {
+          aValue.setNull();
+          return true;
+        }
+      }
+
       domObject = construct(aCx, naviObj);
       if (!domObject) {
         return Throw(aCx, NS_ERROR_FAILURE);
       }
     }
 
     if (!JS_WrapObject(aCx, &domObject)) {
       return false;
--- a/dom/bluetooth/BluetoothService.cpp
+++ b/dom/bluetooth/BluetoothService.cpp
@@ -151,23 +151,27 @@ public:
     if (gInShutdown) {
       gBluetoothService = nullptr;
       return NS_OK;
     }
 
     // Update mEnabled of BluetoothService object since
     // StartInternal/StopInternal have been already done.
     gBluetoothService->SetEnabled(mEnabled);
+    gToggleInProgress = false;
 
     nsAutoString signalName;
     signalName = mEnabled ? NS_LITERAL_STRING("Enabled")
                           : NS_LITERAL_STRING("Disabled");
     BluetoothSignal signal(signalName, NS_LITERAL_STRING(KEY_MANAGER), true);
     gBluetoothService->DistributeSignal(signal);
 
+    // Event 'AdapterAdded' has to be fired after firing 'Enabled'
+    gBluetoothService->TryFiringAdapterAdded();
+
     return NS_OK;
   }
 
 private:
   bool mEnabled;
 };
 
 class BluetoothService::ToggleBtTask : public nsRunnable
@@ -502,16 +506,18 @@ BluetoothService::StartStopBluetooth(boo
   }
 
   if (!mBluetoothThread) {
     mBluetoothThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS,
                                           NS_LITERAL_CSTRING("Bluetooth"),
                                           LazyIdleThread::ManualShutdown);
   }
 
+  mAdapterAddedReceived = false;
+
   nsCOMPtr<nsIRunnable> runnable = new ToggleBtTask(aStart, aIsStartup);
   nsresult rv = mBluetoothThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 void
@@ -542,18 +548,16 @@ BluetoothService::SetEnabled(bool aEnabl
    * aEnabled: expected status of bluetooth
    */
   if (mEnabled == aEnabled) {
     BT_WARNING("Bluetooth has already been enabled/disabled before\
                 or the toggling is failed.");
   }
 
   mEnabled = aEnabled;
-
-  gToggleInProgress = false;
 }
 
 nsresult
 BluetoothService::HandleStartup()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!gToggleInProgress);
 
@@ -783,16 +787,38 @@ BluetoothService::Observe(nsISupports* a
     return HandleShutdown();
   }
 
   MOZ_ASSERT(false, "BluetoothService got unexpected topic!");
   return NS_ERROR_UNEXPECTED;
 }
 
 void
+BluetoothService::TryFiringAdapterAdded()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (IsToggling() || !mAdapterAddedReceived) {
+    return;
+  }
+
+  BluetoothSignal signal(NS_LITERAL_STRING("AdapterAdded"),
+                         NS_LITERAL_STRING(KEY_MANAGER), true);
+  DistributeSignal(signal);
+}
+
+void
+BluetoothService::AdapterAddedReceived()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  mAdapterAddedReceived = true;
+}
+
+void
 BluetoothService::Notify(const BluetoothSignal& aData)
 {
   nsString type = NS_LITERAL_STRING("bluetooth-pairing-request");
 
   AutoSafeJSContext cx;
   JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, nullptr, nullptr, nullptr));
   NS_ENSURE_TRUE_VOID(obj);
 
--- a/dom/bluetooth/BluetoothService.h
+++ b/dom/bluetooth/BluetoothService.h
@@ -312,19 +312,26 @@ public:
   }
 
   bool
   IsToggling() const;
 
   void
   RemoveObserverFromTable(const nsAString& key);
 
+  /**
+   * Below 2 function/variable are used for ensuring event 'AdapterAdded' will
+   * be fired after event 'Enabled'.
+   */
+  void TryFiringAdapterAdded();
+  void AdapterAddedReceived();
+
 protected:
-  BluetoothService()
-  : mEnabled(false)
+  BluetoothService() : mEnabled(false)
+                     , mAdapterAddedReceived(false)
   {
   }
 
   virtual ~BluetoothService();
 
   bool
   Init();
 
@@ -403,13 +410,15 @@ protected:
 private:
   /**
    * Due to the fact that the startup and shutdown of the Bluetooth system
    * can take an indefinite amount of time, a command thread is created
    * that can run blocking calls. The thread is not intended for regular
    * Bluetooth operations though.
    */
   nsCOMPtr<nsIThread> mBluetoothThread;
+
+  bool mAdapterAddedReceived;
 };
 
 END_BLUETOOTH_NAMESPACE
 
 #endif
--- a/dom/bluetooth/bluedroid/gonk/BluetoothServiceBluedroid.cpp
+++ b/dom/bluetooth/bluedroid/gonk/BluetoothServiceBluedroid.cpp
@@ -31,41 +31,16 @@
 #include "mozilla/StaticMutex.h"
 #include "mozilla/StaticPtr.h"
 
 using namespace mozilla;
 using namespace mozilla::ipc;
 USING_BLUETOOTH_NAMESPACE
 
 /**
- *  Classes only used in this file
- */
-class DistributeBluetoothSignalTask : public nsRunnable {
-public:
-  DistributeBluetoothSignalTask(const BluetoothSignal& aSignal) :
-    mSignal(aSignal)
-  {
-  }
-
-  NS_IMETHOD
-  Run()
-  {
-    MOZ_ASSERT(NS_IsMainThread());
-
-    BluetoothService* bs = BluetoothService::Get();
-    bs->DistributeSignal(mSignal);
-
-    return NS_OK;
-  }
-
-private:
-  BluetoothSignal mSignal;
-};
-
-/**
  *  Static variables
  */
 static bluetooth_device_t* sBtDevice;
 static const bt_interface_t* sBtInterface;
 static bool sAdapterDiscoverable = false;
 static bool sIsBtEnabled = false;
 static nsString sAdapterBdAddress;
 static nsString sAdapterBdName;
@@ -77,16 +52,80 @@ static nsTArray<nsRefPtr<BluetoothReplyR
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sChangeDiscoveryRunnableArray;
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sGetDeviceRunnableArray;
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sSetPropertyRunnableArray;
 static nsTArray<nsRefPtr<BluetoothReplyRunnable> > sUnbondingRunnableArray;
 static nsTArray<int> sRequestedDeviceCountArray;
 static StaticAutoPtr<Monitor> sToggleBtMonitor;
 
 /**
+ *  Classes only used in this file
+ */
+class DistributeBluetoothSignalTask : public nsRunnable {
+public:
+  DistributeBluetoothSignalTask(const BluetoothSignal& aSignal) :
+    mSignal(aSignal)
+  {
+  }
+
+  NS_IMETHOD
+  Run()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    BluetoothService* bs = BluetoothService::Get();
+    NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE);
+
+    bs->DistributeSignal(mSignal);
+
+    return NS_OK;
+  }
+
+private:
+  BluetoothSignal mSignal;
+};
+
+class SetupAfterEnabledTask : public nsRunnable
+{
+public:
+  SetupAfterEnabledTask()
+  { }
+
+  NS_IMETHOD
+  Run()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    // Bluetooth scan mode is NONE by default
+    bt_scan_mode_t mode = BT_SCAN_MODE_CONNECTABLE;
+    bt_property_t prop;
+    prop.type = BT_PROPERTY_ADAPTER_SCAN_MODE;
+    prop.val = (void*)&mode;
+    prop.len = sizeof(mode);
+
+    NS_ENSURE_TRUE(sBtInterface, NS_ERROR_FAILURE);
+
+    int ret = sBtInterface->set_adapter_property(&prop);
+    if (ret != BT_STATUS_SUCCESS) {
+      BT_LOGR("%s: Fail to set: BT_SCAN_MODE_CONNECTABLE", __FUNCTION__);
+    }
+
+    // Try to fire event 'AdapterAdded' to fit the original behaviour when
+    // we used BlueZ as backend.
+    BluetoothService* bs = BluetoothService::Get();
+    NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE);
+
+    bs->AdapterAddedReceived();
+    bs->TryFiringAdapterAdded();
+
+    return NS_OK;
+  }
+};
+
+/**
  *  Static callback functions
  */
 static void
 ClassToIcon(uint32_t aClass, nsAString& aRetIcon)
 {
   switch ((aClass & 0x1f00) >> 8) {
     case 0x01:
       aRetIcon.AssignLiteral("computer");
@@ -218,59 +257,32 @@ BdAddressTypeToString(bt_bdaddr_t* aBdAd
   sprintf((char*)bdstr, "%02x:%02x:%02x:%02x:%02x:%02x",
           (int)addr[0],(int)addr[1],(int)addr[2],
           (int)addr[3],(int)addr[4],(int)addr[5]);
 
   aRetBdAddress = NS_ConvertUTF8toUTF16((char*)bdstr);
 }
 
 static void
-Setup()
-{
-  // Bluetooth scan mode is NONE by default
-  bt_scan_mode_t mode = BT_SCAN_MODE_CONNECTABLE;
-  bt_property_t prop;
-  prop.type = BT_PROPERTY_ADAPTER_SCAN_MODE;
-  prop.val = (void*)&mode;
-  prop.len = sizeof(mode);
-
-  NS_ENSURE_TRUE_VOID(sBtInterface);
-
-  int ret = sBtInterface->set_adapter_property(&prop);
-  if (ret != BT_STATUS_SUCCESS) {
-    BT_LOGR("%s: Fail to set: BT_SCAN_MODE_CONNECTABLE", __FUNCTION__);
-  }
-
-  // Event 'AdapterAdded' has to be fired after enabled to notify Gaia
-  // that BluetoothAdapter is ready.
-  BluetoothSignal signal(NS_LITERAL_STRING("AdapterAdded"),
-                         NS_LITERAL_STRING(KEY_MANAGER), true);
-  nsRefPtr<DistributeBluetoothSignalTask>
-    t = new DistributeBluetoothSignalTask(signal);
-  if (NS_FAILED(NS_DispatchToMainThread(t))) {
-    BT_WARNING("Failed to dispatch to main thread!");
-  }
-}
-
-static void
 AdapterStateChangeCallback(bt_state_t aStatus)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
   BT_LOGR("%s, BT_STATE:%d", __FUNCTION__, aStatus);
 
   sIsBtEnabled = (aStatus == BT_STATE_ON);
 
   {
     MonitorAutoLock lock(*sToggleBtMonitor);
     lock.Notify();
   }
 
-  if (sIsBtEnabled) {
-    Setup();
+  if (sIsBtEnabled &&
+      NS_FAILED(NS_DispatchToMainThread(new SetupAfterEnabledTask()))) {
+    BT_WARNING("Failed to dispatch to main thread!");
   }
 }
 
 /**
  * AdapterPropertiesChangeCallback will be called after enable() but before
  * AdapterStateChangeCallback sIsBtEnabled get updated.
  * At that moment, both BluetoothManager/BluetoothAdapter does not register
  * observer yet.
--- a/dom/bluetooth/bluez/linux/BluetoothDBusService.cpp
+++ b/dom/bluetooth/bluez/linux/BluetoothDBusService.cpp
@@ -330,16 +330,34 @@ public:
     hid->HandleInputPropertyChanged(mSignal);
     return NS_OK;
   }
 
 private:
   BluetoothSignal mSignal;
 };
 
+class TryFiringAdapterAddedTask : public nsRunnable
+{
+public:
+  NS_IMETHOD
+  Run()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    BluetoothService* bs = BluetoothService::Get();
+    NS_ENSURE_TRUE(bs, NS_ERROR_FAILURE);
+
+    bs->AdapterAddedReceived();
+    bs->TryFiringAdapterAdded();
+
+    return NS_OK;
+  }
+};
+
 static bool
 IsDBusMessageError(DBusMessage* aMsg, DBusError* aErr, nsAString& aErrorStr)
 {
   if (aErr && dbus_error_is_set(aErr)) {
     aErrorStr = NS_ConvertUTF8toUTF16(aErr->message);
     LOG_AND_FREE_DBUS_ERROR(aErr);
     return true;
   }
@@ -704,21 +722,17 @@ GetProperty(DBusMessageIter aIter, Prope
   } else if (!sAdapterNameIsReady &&
              aPropertyTypes == sAdapterProperties &&
              propertyName.EqualsLiteral("Name")) {
     MOZ_ASSERT(propertyValue.type() == BluetoothValue::TnsString);
 
     // Notify BluetoothManager whenever adapter name is ready.
     if (!propertyValue.get_nsString().IsEmpty()) {
       sAdapterNameIsReady = true;
-      BluetoothSignal signal(NS_LITERAL_STRING("AdapterAdded"),
-                             NS_LITERAL_STRING(KEY_MANAGER), sAdapterPath);
-      nsRefPtr<DistributeBluetoothSignalTask> task =
-        new DistributeBluetoothSignalTask(signal);
-      NS_DispatchToMainThread(task);
+      NS_DispatchToMainThread(new TryFiringAdapterAddedTask());
     }
   }
 
   aProperties.AppendElement(BluetoothNamedValue(propertyName, propertyValue));
   return true;
 }
 
 static void
new file mode 100644
--- /dev/null
+++ b/dom/downloads/moz.build
@@ -0,0 +1,10 @@
+# -*- 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_B2G"]:
+	TEST_DIRS += ['tests']
+
+PARALLEL_DIRS += ['src']
new file mode 100644
--- /dev/null
+++ b/dom/downloads/src/DownloadsAPI.js
@@ -0,0 +1,320 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+Cu.import("resource://gre/modules/DownloadsIPC.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
+function debug(aStr) {
+  dump("-*- DownloadsAPI.js : " + aStr + "\n");
+}
+
+function DOMDownloadManagerImpl() {
+  debug("DOMDownloadManagerImpl constructor");
+}
+
+DOMDownloadManagerImpl.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  // nsIDOMGlobalPropertyInitializer implementation
+  init: function(aWindow) {
+    debug("DownloadsManager init");
+    this.initDOMRequestHelper(aWindow,
+                              ["Downloads:Added",
+                               "Downloads:Removed"]);
+  },
+
+  uninit: function() {
+    debug("uninit");
+    downloadsCache.evict(this._window);
+  },
+
+  set ondownloadstart(aHandler) {
+    this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler);
+  },
+
+  get ondownloadstart() {
+    return this.__DOM_IMPL__.getEventHandler("ondownloadstart");
+  },
+
+  getDownloads: function() {
+    debug("getDownloads()");
+
+    return this.createPromise(function (aResolve, aReject) {
+      DownloadsIPC.getDownloads().then(
+        function(aDownloads) {
+          // Turn the list of download objects into DOM objects and
+          // send them.
+          let array = Cu.createArrayIn(this._window);
+          for (let id in aDownloads) {
+            let dom = createDOMDownloadObject(this._window, aDownloads[id]);
+            array.push(this._prepareForContent(dom));
+          }
+          aResolve(array);
+        }.bind(this),
+        function() {
+          aReject("GetDownloadsError");
+        }
+      );
+    }.bind(this));
+  },
+
+  clearAllDone: function() {
+    debug("clearAllDone()");
+    return this.createPromise(function (aResolve, aReject) {
+      DownloadsIPC.clearAllDone().then(
+        function(aDownloads) {
+          // Turn the list of download objects into DOM objects and
+          // send them.
+          let array = Cu.createArrayIn(this._window);
+          for (let id in aDownloads) {
+            let dom = createDOMDownloadObject(this._window, aDownloads[id]);
+            array.push(this._prepareForContent(dom));
+          }
+          aResolve(array);
+        }.bind(this),
+        function() {
+          aReject("ClearAllDoneError");
+        }
+      );
+    }.bind(this));
+  },
+
+  remove: function(aDownload) {
+    debug("remove " + aDownload.url + " " + aDownload.id);
+    return this.createPromise(function (aResolve, aReject) {
+      if (!downloadsCache.has(this._window, aDownload.id)) {
+        debug("no download " + aDownload.id);
+        aReject("InvalidDownload");
+        return;
+      }
+
+      DownloadsIPC.remove(aDownload.id).then(
+        function(aResult) {
+          let dom = createDOMDownloadObject(this._window, aResult);
+          // Change the state right away to not race against the update message.
+          dom.wrappedJSObject.state = "finalized";
+          aResolve(this._prepareForContent(dom));
+        }.bind(this),
+        function() {
+          aReject("RemoveError");
+        }
+      );
+    }.bind(this));
+  },
+
+  /**
+    * Turns a chrome download object into a content accessible one.
+    * When we have __DOM_IMPL__ available we just use that, otherwise
+    * we run _create() with the wrapped js object.
+    */
+  _prepareForContent: function(aChromeObject) {
+    if (aChromeObject.__DOM_IMPL__) {
+      return aChromeObject.__DOM_IMPL__;
+    }
+    let res = this._window.DOMDownload._create(this._window,
+                                            aChromeObject.wrappedJSObject);
+    return res;
+  },
+
+  receiveMessage: function(aMessage) {
+    let data = aMessage.data;
+    switch(aMessage.name) {
+      case "Downloads:Added":
+        debug("Adding " + uneval(data));
+        let event = new this._window.DownloadEvent("downloadstart", {
+          download:
+            this._prepareForContent(createDOMDownloadObject(this._window, data))
+        });
+        this.__DOM_IMPL__.dispatchEvent(event);
+        break;
+    }
+  },
+
+  classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+
+};
+
+/**
+  * Keep track of download objects per window.
+  */
+let downloadsCache = {
+  init: function() {
+    this.cache = new WeakMap();
+  },
+
+  has: function(aWindow, aId) {
+    let downloads = this.cache.get(aWindow);
+    return !!(downloads && downloads[aId]);
+  },
+
+  get: function(aWindow, aDownload) {
+    let downloads = this.cache.get(aWindow);
+    if (!(downloads && downloads[aDownload.id])) {
+      debug("Adding download " + aDownload.id + " to cache.");
+      if (!downloads) {
+        this.cache.set(aWindow, {});
+        downloads = this.cache.get(aWindow);
+      }
+      // Create the object and add it to the cache.
+      let impl = Cc["@mozilla.org/downloads/download;1"]
+                   .createInstance(Ci.nsISupports);
+      impl.wrappedJSObject._init(aWindow, aDownload);
+      downloads[aDownload.id] = impl;
+    }
+    return downloads[aDownload.id];
+  },
+
+  evict: function(aWindow) {
+    this.cache.delete(aWindow);
+  }
+};
+
+downloadsCache.init();
+
+/**
+  * The DOM facade of a download object.
+  */
+
+function createDOMDownloadObject(aWindow, aDownload) {
+  return downloadsCache.get(aWindow, aDownload);
+}
+
+function DOMDownloadImpl() {
+  debug("DOMDownloadImpl constructor ");
+  this.wrappedJSObject = this;
+  this.totalBytes = 0;
+  this.currentBytes = 0;
+  this.url = null;
+  this.path = null;
+  this.state = "stopped";
+  this.contentType = null;
+  this.startTime = Date.now();
+  this.error = null;
+
+  /* private fields */
+  this.id = null;
+}
+
+DOMDownloadImpl.prototype = {
+
+  createPromise: function(aPromiseInit) {
+    return new this._window.Promise(aPromiseInit);
+  },
+
+  pause: function() {
+    debug("DOMDownloadImpl pause");
+    let id = this.id;
+    // We need to wrap the Promise.jsm promise in a "real" DOM promise...
+    return this.createPromise(function(aResolve, aReject) {
+      DownloadsIPC.pause(id).then(aResolve, aReject);
+    });
+  },
+
+  resume: function() {
+    debug("DOMDownloadImpl resume");
+    let id = this.id;
+    // We need to wrap the Promise.jsm promise in a "real" DOM promise...
+    return this.createPromise(function(aResolve, aReject) {
+      DownloadsIPC.resume(id).then(aResolve, aReject);
+    });
+  },
+
+  set onstatechange(aHandler) {
+    this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler);
+  },
+
+  get onstatechange() {
+    return this.__DOM_IMPL__.getEventHandler("onstatechange");
+  },
+
+  _init: function(aWindow, aDownload) {
+    this._window = aWindow;
+    this.id = aDownload.id;
+    this._update(aDownload);
+    Services.obs.addObserver(this, "downloads-state-change-" + this.id,
+                             /* ownsWeak */ true);
+    debug("observer set for " + this.id);
+  },
+
+  /**
+    * Updates the state of the object and fires the statechange event.
+    */
+  _update: function(aDownload) {
+    debug("update " + uneval(aDownload));
+    if (this.id != aDownload.id) {
+      return;
+    }
+
+    let props = ["totalBytes", "currentBytes", "url", "path", "state",
+                 "contentType", "startTime"];
+    let changed = false;
+
+    props.forEach((prop) => {
+      if (aDownload[prop] && (aDownload[prop] != this[prop])) {
+        this[prop] = aDownload[prop];
+        changed = true;
+      }
+    });
+
+    if (aDownload.error) {
+      this.error = new this._window.DOMError("DownloadError", aDownload.error);
+    } else {
+      this.error = null;
+    }
+
+    // The visible state has not changed, so no need to fire an event.
+    if (!changed) {
+      return;
+    }
+
+    // __DOM_IMPL__ may not be available at first update.
+    if (this.__DOM_IMPL__) {
+      let event = new this._window.DownloadEvent("statechange", {
+        download: this.__DOM_IMPL__
+      });
+      debug("Dispatching statechange event. state=" + this.state);
+      this.__DOM_IMPL__.dispatchEvent(event);
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    debug("DOMDownloadImpl observe " + aTopic);
+    if (aTopic !== "downloads-state-change-" + this.id) {
+      return;
+    }
+
+    try {
+      let download = JSON.parse(aData);
+      // We get the start time as milliseconds, not as a Date object.
+      if (download.startTime) {
+        download.startTime = new Date(download.startTime);
+      }
+      this._update(download);
+    } catch(e) {}
+  },
+
+  classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+                                         Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl,
+                                                     DOMDownloadImpl]);
new file mode 100644
--- /dev/null
+++ b/dom/downloads/src/DownloadsAPI.jsm
@@ -0,0 +1,255 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageBroadcaster");
+
+function debug(aStr) {
+  dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
+}
+
+function sendPromiseMessage(aMm, aMessageName, aData, aError) {
+  debug("sendPromiseMessage " + aMessageName);
+  let msg = {
+    id: aData.id,
+    promiseId: aData.promiseId
+  };
+
+  if (aError) {
+    msg.error = aError;
+  }
+
+  aMm.sendAsyncMessage(aMessageName, msg);
+}
+
+let DownloadsAPI = {
+  init: function() {
+    debug("init");
+
+    this._ids = new WeakMap(); // Maps toolkit download objects to ids.
+    this._index = {};          // Maps ids to downloads.
+
+    ["Downloads:GetList",
+     "Downloads:ClearAllDone",
+     "Downloads:Remove",
+     "Downloads:Pause",
+     "Downloads:Resume"].forEach((msgName) => {
+      ppmm.addMessageListener(msgName, this);
+    });
+
+    let self = this;
+    Task.spawn(function () {
+      let list = yield Downloads.getList(Downloads.ALL);
+      yield list.addView(self);
+
+      debug("view added to download list.");
+    }).then(null, Components.utils.reportError);
+
+    this._currentId = 0;
+  },
+
+  /**
+    * Returns a unique id for each download, hashing the url and the path.
+    */
+  downloadId: function(aDownload) {
+    let id = this._ids.get(aDownload, null);
+    if (!id) {
+      id = "download-" + this._currentId++;
+      this._ids.set(aDownload, id);
+      this._index[id] = aDownload;
+    }
+    return id;
+  },
+
+  getDownloadById: function(aId) {
+    return this._index[aId];
+  },
+
+  /**
+    * Converts a download object into a plain json object that we'll
+    * send to the DOM side.
+    */
+  jsonDownload: function(aDownload) {
+    let res = {
+      totalBytes: aDownload.totalBytes,
+      currentBytes: aDownload.currentBytes,
+      url: aDownload.source.url,
+      path: aDownload.target.path,
+      contentType: aDownload.contentType,
+      startTime: aDownload.startTime.getTime()
+    };
+
+    if (aDownload.error) {
+      res.error = aDownload.error.name;
+    }
+
+    res.id = this.downloadId(aDownload);
+
+    // The state of the download. Can be any of "downloading", "stopped",
+    // "succeeded", finalized".
+
+    // Default to "stopped"
+    res.state = "stopped";
+    if (!aDownload.stopped &&
+        !aDownload.canceled &&
+        !aDownload.succeeded &&
+        !aDownload.DownloadError) {
+      res.state = "downloading";
+    } else if (aDownload.succeeded) {
+      res.state = "succeeded";
+    }
+    return res;
+  },
+
+  /**
+    * download view methods.
+    */
+  onDownloadAdded: function(aDownload) {
+    let download = this.jsonDownload(aDownload);
+    debug("onDownloadAdded " + uneval(download));
+    ppmm.broadcastAsyncMessage("Downloads:Added", download);
+  },
+
+  onDownloadRemoved: function(aDownload) {
+    let download = this.jsonDownload(aDownload);
+    download.state = "finalized";
+    debug("onDownloadRemoved " + uneval(download));
+    ppmm.broadcastAsyncMessage("Downloads:Removed", download);
+    this._index[this._ids.get(aDownload)] = null;
+    this._ids.delete(aDownload);
+  },
+
+  onDownloadChanged: function(aDownload) {
+    let download = this.jsonDownload(aDownload);
+    debug("onDownloadChanged " + uneval(download));
+    ppmm.broadcastAsyncMessage("Downloads:Changed", download);
+  },
+
+  receiveMessage: function(aMessage) {
+    if (!aMessage.target.assertPermission("downloads")) {
+      debug("No 'downloads' permission!");
+      return;
+    }
+
+    debug("message: " + aMessage.name);
+    // Removing 'Downloads:' and turning first letter to lower case to
+    // build the function name from the message name.
+    let c = aMessage.name[10].toLowerCase();
+    let methodName = c + aMessage.name.substring(11);
+    if (this[methodName] && typeof this[methodName] === "function") {
+      this[methodName](aMessage.data, aMessage.target);
+    } else {
+      debug("Unimplemented method:  " + methodName);
+    }
+  },
+
+  getList: function(aData, aMm) {
+    debug("getList called!");
+    let self = this;
+    Task.spawn(function () {
+      let list = yield Downloads.getList(Downloads.ALL);
+      let downloads = yield list.getAll();
+      let res = [];
+      downloads.forEach((aDownload) => {
+        res.push(self.jsonDownload(aDownload));
+      });
+      aMm.sendAsyncMessage("Downloads:GetList:Return", res);
+    }).then(null, Components.utils.reportError);
+  },
+
+  clearAllDone: function(aData, aMm) {
+    debug("clearAllDone called!");
+    let self = this;
+    Task.spawn(function () {
+      let list = yield Downloads.getList(Downloads.ALL);
+      yield list.removeFinished();
+      list = yield Downloads.getList(Downloads.ALL);
+      let downloads = yield list.getAll();
+      let res = [];
+      downloads.forEach((aDownload) => {
+        res.push(self.jsonDownload(aDownload));
+      });
+      aMm.sendAsyncMessage("Downloads:ClearAllDone:Return", res);
+    }).then(null, Components.utils.reportError);
+  },
+
+  remove: function(aData, aMm) {
+    debug("remove id " + aData.id);
+    let download = this.getDownloadById(aData.id);
+    if (!download) {
+      sendPromiseMessage(aMm, "Downloads:Remove:Return",
+                         aData, "NoSuchDownload");
+      return;
+    }
+
+    Task.spawn(function() {
+      yield download.finalize(true);
+      let list = yield Downloads.getList(Downloads.ALL);
+      yield list.remove(download);
+    }).then(
+      function() {
+        sendPromiseMessage(aMm, "Downloads:Remove:Return", aData);
+      },
+      function() {
+        sendPromiseMessage(aMm, "Downloads:Remove:Return",
+                           aData, "RemoveError");
+      }
+    );
+  },
+
+  pause: function(aData, aMm) {
+    debug("pause id " + aData.id);
+    let download = this.getDownloadById(aData.id);
+    if (!download) {
+      sendPromiseMessage(aMm, "Downloads:Pause:Return",
+                         aData, "NoSuchDownload");
+      return;
+    }
+
+    download.cancel().then(
+      function() {
+        sendPromiseMessage(aMm, "Downloads:Pause:Return", aData);
+      },
+      function() {
+        sendPromiseMessage(aMm, "Downloads:Pause:Return",
+                           aData, "PauseError");
+      }
+    );
+  },
+
+  resume: function(aData, aMm) {
+    debug("resume id " + aData.id);
+    let download = this.getDownloadById(aData.id);
+    if (!download) {
+      sendPromiseMessage(aMm, "Downloads:Resume:Return",
+                         aData, "NoSuchDownload");
+      return;
+    }
+
+    download.start().then(
+      function() {
+        sendPromiseMessage(aMm, "Downloads:Resume:Return", aData);
+      },
+      function() {
+        sendPromiseMessage(aMm, "Downloads:Resume:Return",
+                           aData, "ResumeError");
+      }
+    );
+  }
+};
+
+DownloadsAPI.init();
new file mode 100644
--- /dev/null
+++ b/dom/downloads/src/DownloadsAPI.manifest
@@ -0,0 +1,6 @@
+# DownloadsAPI.js
+component {c6587afa-0696-469f-9eff-9dac0dd727fe} DownloadsAPI.js
+contract @mozilla.org/downloads/manager;1 {c6587afa-0696-469f-9eff-9dac0dd727fe}
+
+component {96b81b99-aa96-439d-8c59-92eeed34705f} DownloadsAPI.js
+contract @mozilla.org/downloads/download;1 {96b81b99-aa96-439d-8c59-92eeed34705f}
new file mode 100644
--- /dev/null
+++ b/dom/downloads/src/DownloadsIPC.jsm
@@ -0,0 +1,221 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["DownloadsIPC"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
+/**
+  * This module lives in the child process and receives the ipc messages
+  * from the parent. It saves the download's state and redispatch changes
+  * to DOM objects using an observer notification.
+  *
+  * This module needs to be loaded once and only once per process.
+  */
+
+function debug(aStr) {
+  dump("-*- DownloadsIPC.jsm : " + aStr + "\n");
+}
+
+const ipcMessages = ["Downloads:Added",
+                     "Downloads:Removed",
+                     "Downloads:Changed",
+                     "Downloads:GetList:Return",
+                     "Downloads:ClearAllDone:Return",
+                     "Downloads:Remove:Return",
+                     "Downloads:Pause:Return",
+                     "Downloads:Resume:Return"];
+
+this.DownloadsIPC = {
+  downloads: {},
+
+  init: function() {
+    debug("init");
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+    ipcMessages.forEach((aMessage) => {
+      cpmm.addMessageListener(aMessage, this);
+    });
+
+    // We need to get the list of current downloads.
+    this.ready = false;
+    this.getListPromises = [];
+    this.clearAllPromises = [];
+    this.downloadPromises = {};
+    cpmm.sendAsyncMessage("Downloads:GetList", {});
+    this._promiseId = 0;
+  },
+
+  notifyChanges: function(aId) {
+    // TODO: use the subject instead of stringifying.
+    if (this.downloads[aId]) {
+      debug("notifyChanges notifying changes for " + aId);
+      Services.obs.notifyObservers(null, "downloads-state-change-" + aId,
+                                   JSON.stringify(this.downloads[aId]));
+    } else {
+      debug("notifyChanges failed for " + aId)
+    }
+  },
+
+  _updateDownloadsArray: function(aDownloads) {
+    this.downloads = [];
+    // We actually have an array of downloads.
+    aDownloads.forEach((aDownload) => {
+      this.downloads[aDownload.id] = aDownload;
+    });
+  },
+
+  receiveMessage: function(aMessage) {
+    let download = aMessage.data;
+    debug("message: " + aMessage.name + " " + download.id);
+    switch(aMessage.name) {
+      case "Downloads:GetList:Return":
+        this._updateDownloadsArray(download);
+
+        if (!this.ready) {
+          this.getListPromises.forEach(aPromise =>
+                                       aPromise.resolve(this.downloads));
+          this.getListPromises.length = 0;
+        }
+        this.ready = true;
+        break;
+      case "Downloads:ClearAllDone:Return":
+        this._updateDownloadsArray(download);
+        this.clearAllPromises.forEach(aPromise =>
+                                      aPromise.resolve(this.downloads));
+        this.clearAllPromises.length = 0;
+        break;
+      case "Downloads:Added":
+        this.downloads[download.id] = download;
+        this.notifyChanges(download.id);
+        break;
+      case "Downloads:Removed":
+        if (this.downloads[download.id]) {
+          this.downloads[download.id] = download;
+          this.notifyChanges(download.id);
+          delete this.downloads[download.id];
+        }
+        break;
+      case "Downloads:Changed":
+        // Only update properties that actually changed.
+        let cached = this.downloads[download.id];
+        if (!cached) {
+          debug("No download found for " + download.id);
+          return;
+        }
+        let props = ["totalBytes", "currentBytes", "url", "path", "state",
+                     "contentType", "startTime"];
+        let changed = false;
+
+        props.forEach((aProp) => {
+          if (download[aProp] && (download[aProp] != cached[aProp])) {
+            cached[aProp] = download[aProp];
+            changed = true;
+          }
+        });
+
+        // Updating the error property. We always get a 'state' change as
+        // well.
+        cached.error = download.error;
+
+        if (changed) {
+          this.notifyChanges(download.id);
+        }
+        break;
+      case "Downloads:Remove:Return":
+      case "Downloads:Pause:Return":
+      case "Downloads:Resume:Return":
+        if (this.downloadPromises[download.promiseId]) {
+          if (!download.error) {
+          this.downloadPromises[download.promiseId].resolve(download);
+          } else {
+            this.downloadPromises[download.promiseId].reject(download);
+          }
+          delete this.downloadPromises[download.promiseId];
+        }
+        break;
+    }
+  },
+
+  /**
+    * Returns a promise that is resolved with the list of current downloads.
+    */
+  getDownloads: function() {
+    debug("getDownloads()");
+    let deferred = Promise.defer();
+    if (this.ready) {
+      debug("Returning existing list.");
+      deferred.resolve(this.downloads);
+    } else {
+      this.getListPromises.push(deferred);
+    }
+    return deferred.promise;
+  },
+
+  /**
+    * Returns a promise that is resolved with the list of current downloads.
+    */
+  clearAllDone: function() {
+    debug("clearAllDone");
+    let deferred = Promise.defer();
+    this.clearAllPromises.push(deferred);
+    cpmm.sendAsyncMessage("Downloads:ClearAllDone", {});
+    return deferred.promise;
+  },
+
+  promiseId: function() {
+    return this._promiseId++;
+  },
+
+  remove: function(aId) {
+    debug("remove " + aId);
+    let deferred = Promise.defer();
+    let pId = this.promiseId();
+    this.downloadPromises[pId] = deferred;
+    cpmm.sendAsyncMessage("Downloads:Remove",
+                          { id: aId, promiseId: pId });
+    return deferred.promise;
+  },
+
+  pause: function(aId) {
+    debug("pause " + aId);
+    let deferred = Promise.defer();
+    let pId = this.promiseId();
+    this.downloadPromises[pId] = deferred;
+    cpmm.sendAsyncMessage("Downloads:Pause",
+                          { id: aId, promiseId: pId });
+    return deferred.promise;
+  },
+
+  resume: function(aId) {
+    debug("resume " + aId);
+    let deferred = Promise.defer();
+    let pId = this.promiseId();
+    this.downloadPromises[pId] = deferred;
+    cpmm.sendAsyncMessage("Downloads:Resume",
+                          { id: aId, promiseId: pId });
+    return deferred.promise;
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic == "xpcom-shutdown") {
+      ipcMessages.forEach((aMessage) => {
+        cpmm.removeMessageListener(aMessage, this);
+      });
+    }
+  }
+};
+
+DownloadsIPC.init();
new file mode 100644
--- /dev/null
+++ b/dom/downloads/src/moz.build
@@ -0,0 +1,15 @@
+# -*- 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/.
+
+EXTRA_COMPONENTS += [
+    'DownloadsAPI.js',
+    'DownloadsAPI.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'DownloadsAPI.jsm',
+    'DownloadsIPC.jsm',
+]
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/mochitest.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+  serve_file.sjs
+
+[test_downloads_navigator_object.html]
+[test_downloads_basic.html]
+[test_downloads_large.html]
+[test_downloads_pause_remove.html]
+[test_downloads_pause_resume.html]
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+MOCHITEST_MANIFESTS += ['mochitest.ini']
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/serve_file.sjs
@@ -0,0 +1,107 @@
+// Serves a file with a given mime type and size at an optionally given rate.
+
+function getQuery(request) {
+  var query = {};
+  request.queryString.split('&').forEach(function (val) {
+    var [name, value] = val.split('=');
+    query[name] = unescape(value);
+  });
+  return query;
+}
+
+// Timer used to handle the request response.
+var timer = null;
+
+function handleResponse() {
+  // Is this a rate limited response?
+  if (this.state.rate > 0) {
+    // Calculate how many bytes we have left to send.
+    var bytesToWrite = this.state.totalBytes - this.state.sentBytes;
+
+    // Do we have any bytes left to send? If not we'll just fall thru and
+    // cancel our repeating timer and finalize the response.
+    if (bytesToWrite > 0) {
+      // Figure out how many bytes to send, based on the rate limit.
+      bytesToWrite =
+        (bytesToWrite > this.state.rate) ? this.state.rate : bytesToWrite;
+
+      for (let i = 0; i < bytesToWrite; i++) {
+        this.response.write("0");
+      }
+
+      // Update the number of bytes we've sent to the client.
+      this.state.sentBytes += bytesToWrite;
+
+      // Wait until the next call to do anything else.
+      return;
+    }
+  }
+  else {
+    // Not rate limited, write it all out.
+    for (let i = 0; i < this.state.totalBytes; i++) {
+      this.response.write("0");
+    }
+  }
+
+  // Finalize the response.
+  this.response.finish();
+
+  // All done sending, go ahead and cancel our repeating timer.
+  timer.cancel();
+}
+
+function handleRequest(request, response) {
+  var query = getQuery(request);
+
+  // Default values for content type, size and rate.
+  var contentType = "text/plain";
+  var size = 1024;
+  var rate = 0;
+
+  // optional content type to be used by our response.
+  if ("contentType" in query) {
+    contentType = query["contentType"];
+  }
+
+  // optional size (in bytes) for generated file.
+  if ("size" in query) {
+    size = parseInt(query["size"]);
+  }
+
+  // optional rate (in bytes/s) at which to send the file.
+  if ("rate" in query) {
+    rate = parseInt(query["rate"]);
+  }
+
+  // The context for the responseHandler.
+  var context = {
+    response: response,
+    state: {
+      contentType: contentType,
+      totalBytes: size,
+      sentBytes: 0,
+      rate: rate
+    }
+  };
+
+  // The notify implementation for the timer.
+  context.notify = handleResponse.bind(context);
+
+  timer =
+    Components.classes["@mozilla.org/timer;1"]
+              .createInstance(Components.interfaces.nsITimer);
+
+  // sending at a specific rate requires our response to be asynchronous so
+  // we handle all requests asynchronously. See handleResponse().
+  response.processAsync();
+
+  // generate the content.
+  response.setHeader("Content-Type", contentType, false);
+  response.setHeader("Content-Length", size.toString(), false);
+
+  // initialize the timer and start writing out the response.
+  timer.initWithCallback(context,
+                         1000,
+                         Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+}
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/test_downloads_basic.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=938023
+-->
+<head>
+  <title>Test for Bug 938023 Downloads API</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<a href="serve_file.sjs?contentType=application/octet-stream&size=1024" download="test.bin" id="download1">Download #1</a>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+// Testing a simple download, waiting for it to be done.
+
+SimpleTest.waitForExplicitFinish();
+
+var index = -1;
+
+function next() {
+  index += 1;
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+function downloadChange(evt) {
+  var download = evt.download;
+  if (download.state == "succeeded") {
+    ok(download.totalBytes == 1024, "Download size is 1024 bytes.");
+    ok(download.contentType == "application/octet-stream",
+       "contentType is application/octet-stream.");
+    SimpleTest.finish();
+  }
+}
+
+var steps = [
+  // Start by setting the pref to true.
+  function() {
+    SpecialPowers.pushPrefEnv({
+      set: [["dom.mozDownloads.enabled", true]]
+    }, next);
+  },
+
+  // Setup the event listeners.
+  function() {
+    SpecialPowers.pushPermissions([
+      {type: "downloads", allow: true, context: document}
+    ], function() {
+      navigator.mozDownloadManager.ondownloadstart =
+        function(evt) {
+          ok(true, "Download started");
+          evt.download.addEventListener("statechange", downloadChange);
+        }
+      next();
+    });
+  },
+
+  // Click on the <a download> to start the download.
+  function() {
+    document.getElementById("download1").click();
+  }
+];
+
+next();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/test_downloads_large.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=938023
+-->
+<head>
+  <title>Test for Bug 938023 Downloads API</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<a href="serve_file.sjs?contentType=application/octet-stream&size=102400" download="test.bin" id="download1">Large Download</a>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+// Testing downloading a file, then checking getDownloads() and clearAllDone().
+
+SimpleTest.waitForExplicitFinish();
+
+var index = -1;
+
+function next(args) {
+  index += 1;
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index](args);
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+// Catch all error function.
+function error() {
+  ok(false, "API failure");
+  SimpleTest.finish();
+}
+
+function getDownloads(downloads) {
+  ok(downloads.length == 1, "One downloads after getDownloads");
+  navigator.mozDownloadManager.clearAllDone().then(clearAllDone, error);
+}
+
+function clearAllDone(downloads) {
+  ok(downloads.length == 0, "No downloads after clearAllDone");
+  SimpleTest.finish();
+}
+
+function downloadChange(evt) {
+  var download = evt.download;
+
+  if (download.state == "succeeded") {
+    ok(download.totalBytes == 102400, "Download size is 100k bytes.");
+    navigator.mozDownloadManager.getDownloads().then(getDownloads, error);
+  }
+}
+
+var steps = [
+  // Start by setting the pref to true.
+  function() {
+    SpecialPowers.pushPrefEnv({
+      set: [["dom.mozDownloads.enabled", true]]
+    }, next);
+  },
+
+  // Setup permission and clear current list.
+  function() {
+    SpecialPowers.pushPermissions([
+      {type: "downloads", allow: true, context: document}
+    ], function() {
+      navigator.mozDownloadManager.clearAllDone().then(next, error);
+    });
+  },
+
+  function(downloads) {
+    ok(downloads.length == 0, "Start with an empty download list.");
+    next();
+  },
+
+  // Setup the event listeners.
+  function() {
+    navigator.mozDownloadManager.ondownloadstart =
+      function(evt) {
+        ok(true, "Download started");
+        evt.download.addEventListener("statechange", downloadChange);
+      }
+    next();
+  },
+
+  // Click on the <a download> to start the download.
+  function() {
+    document.getElementById("download1").click();
+  }
+];
+
+next();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/test_downloads_navigator_object.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=938023
+-->
+<head>
+  <title>Test for Bug 938023 Downloads API</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+SimpleTest.waitForExplicitFinish();
+
+var index = -1;
+
+function next() {
+  index += 1;
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+var steps = [
+  // Start by setting the pref to true.
+  function() {
+    SpecialPowers.pushPrefEnv({
+      set: [["dom.mozDownloads.enabled", true]]
+    }, next);
+  },
+
+  function() {
+    SpecialPowers.pushPermissions([
+      {type: "downloads", allow: 0, context: document}
+    ], function() {
+      ise(frames[0].navigator.mozDownloadManager, null, "navigator.mozDownloadManager is null when the page doesn't have permissions");
+      next();
+    });
+  },
+
+  function() {
+    SpecialPowers.pushPrefEnv({
+      set: [["dom.mozDownloads.enabled", false]]
+    }, function() {
+      ise(navigator.mozDownloadManager, undefined, "navigator.mozDownloadManager is undefined");
+      next();
+    });
+  },
+
+  function() {
+    SimpleTest.finish();
+  }
+];
+
+next();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/test_downloads_pause_remove.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=938023
+-->
+<head>
+  <title>Test for Bug 938023 Downloads API</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<a href="serve_file.sjs?contentType=application/octet-stream&size=102400&rate=1024" download="test.bin" id="download1">Large Download</a>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+// Testing pausing a download and then removing it.
+
+SimpleTest.waitForExplicitFinish();
+
+var index = -1;
+
+function next(args) {
+  index += 1;
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index](args);
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+var pausing = false;
+
+// Catch all error function.
+function error() {
+  ok(false, "API failure");
+  SimpleTest.finish();
+}
+
+function checkDownloadList(downloads) {
+  ok(downloads.length == 0, "No downloads left");
+  SimpleTest.finish();
+}
+
+function checkRemoved(download) {
+  ok(download.state == "finalized", "Download removed.");
+  navigator.mozDownloadManager.getDownloads()
+           .then(checkDownloadList, error);
+}
+
+function downloadChange(evt) {
+  var download = evt.download;
+
+  if (download.state == "downloading" && !pausing) {
+    pausing = true;
+    download.pause();
+  } else if (download.state == "stopped") {
+    ok(pausing, "Download stopped by pause()");
+    navigator.mozDownloadManager.remove(download)
+             .then(checkRemoved, error);
+  }
+}
+
+var steps = [
+  // Start by setting the pref to true.
+  function() {
+    SpecialPowers.pushPrefEnv({
+      set: [["dom.mozDownloads.enabled", true]]
+    }, next);
+  },
+
+  // Setup permission and clear current list.
+  function() {
+    SpecialPowers.pushPermissions([
+      {type: "downloads", allow: true, context: document}
+    ], function() {
+      navigator.mozDownloadManager.clearAllDone().then(next, error);
+    });
+  },
+
+  function(downloads) {
+    ok(downloads.length == 0, "Start with an empty download list.");
+    next();
+  },
+
+  // Setup the event listeners.
+  function() {
+    navigator.mozDownloadManager.ondownloadstart =
+      function(evt) {
+        ok(true, "Download started");
+        evt.download.addEventListener("statechange", downloadChange);
+      }
+    next();
+  },
+
+  // Click on the <a download> to start the download.
+  function() {
+    document.getElementById("download1").click();
+  }
+];
+
+next();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/downloads/tests/test_downloads_pause_resume.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=938023
+-->
+<head>
+  <title>Test for Bug 938023 Downloads API</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<a href="serve_file.sjs?contentType=application/octet-stream&size=102400&rate=1024" download="test.bin" id="download1">Large Download</a>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+// Testing pausing a download and then resuming it.
+
+SimpleTest.waitForExplicitFinish();
+
+var index = -1;
+
+function next(args) {
+  index += 1;
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index](args);
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+}
+
+var pausing = false;
+var resuming = false;
+
+// Catch all error function.
+function error() {
+  ok(false, "API failure");
+  SimpleTest.finish();
+}
+
+function checkDownloadList(downloads) {
+  ok(downloads.length == 0, "No downloads left");
+  SimpleTest.finish();
+}
+
+function checkResumedFailed(download) {
+  ok(download.state == "stopped", "Download fails to resume.");
+  navigator.mozDownloadManager.clearAllDone()
+           .then(checkDownloadList, error);
+}
+
+function downloadChange(evt) {
+  var download = evt.download;
+
+  if (download.state == "downloading" && !pausing) {
+    pausing = true;
+    download.pause();
+  } else if (download.state == "stopped" && !resuming) {
+    resuming = true;
+    ok(pausing, "Download stopped by pause()");
+    // serve_file.sjs does not support resuming, so that should fail.
+    download.resume()
+            .then(error, function() { checkResumedFailed(download); });
+  }
+}
+
+var steps = [
+  // Start by setting the pref to true.
+  function() {
+    SpecialPowers.pushPrefEnv({
+      set: [["dom.mozDownloads.enabled", true]]
+    }, next);
+  },
+
+  // Setup permission and clear current list.
+  function() {
+    SpecialPowers.pushPermissions([
+      {type: "downloads", allow: true, context: document}
+    ], function() {
+      navigator.mozDownloadManager.clearAllDone().then(next, error);
+    });
+  },
+
+  function(downloads) {
+    ok(downloads.length == 0, "Start with an empty download list.");
+    next();
+  },
+
+  // Setup the event listeners.
+  function() {
+    navigator.mozDownloadManager.ondownloadstart =
+      function(evt) {
+        ok(true, "Download started");
+        evt.download.addEventListener("statechange", downloadChange);
+      }
+    next();
+  },
+
+  // Click on the <a download> to start the download.
+  function() {
+    document.getElementById("download1").click();
+  }
+];
+
+next();
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/icc/tests/marionette/test_icc_contact.js
+++ b/dom/icc/tests/marionette/test_icc_contact.js
@@ -1,36 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 MARIONETTE_TIMEOUT = 60000;
 MARIONETTE_HEAD_JS = "icc_header.js";
 
+const EMULATOR_ICCID = "89014103211118510720";
+
 function testReadContacts(type) {
   let request = icc.readContacts(type);
   request.onsuccess = function onsuccess() {
     let contacts = request.result;
 
     is(Array.isArray(contacts), true);
 
     is(contacts[0].name[0], "Mozilla");
     is(contacts[0].tel[0].value, "15555218201");
-    is(contacts[0].id, "890141032111185107201");
+    is(contacts[0].id, EMULATOR_ICCID + "1");
 
     is(contacts[1].name[0], "Saßê黃");
     is(contacts[1].tel[0].value, "15555218202");
-    is(contacts[1].id, "890141032111185107202");
+    is(contacts[1].id, EMULATOR_ICCID + "2");
 
     is(contacts[2].name[0], "Fire 火");
     is(contacts[2].tel[0].value, "15555218203");
-    is(contacts[2].id, "890141032111185107203");
+    is(contacts[2].id, EMULATOR_ICCID + "3");
 
     is(contacts[3].name[0], "Huang 黃");
     is(contacts[3].tel[0].value, "15555218204");
-    is(contacts[3].id, "890141032111185107204");
+    is(contacts[3].id, EMULATOR_ICCID + "4");
 
     taskHelper.runNext();
   };
 
   request.onerror = function onerror() {
     ok(false, "Cannot get " + type + " contacts");
     taskHelper.runNext();
   };
@@ -41,16 +43,21 @@ function testAddContact(type, pin2) {
     name: ["add"],
     tel: [{value: "0912345678"}],
     email:[]
   });
 
   let updateRequest = icc.updateContact(type, contact, pin2);
 
   updateRequest.onsuccess = function onsuccess() {
+    let updatedContact = updateRequest.result;
+    ok(updatedContact, "updateContact should have retuend a mozContact.");
+    ok(updatedContact.id.startsWith(EMULATOR_ICCID),
+       "The returned mozContact has wrong id.");
+
     // Get ICC contact for checking new contact
 
     let getRequest = icc.readContacts(type);
 
     getRequest.onsuccess = function onsuccess() {
       let contacts = getRequest.result;
 
       // There are 4 SIM contacts which are harded in emulator
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -99,16 +99,19 @@ if CONFIG['MOZ_PAY']:
     PARALLEL_DIRS += ['payment']
 
 if CONFIG['MOZ_GAMEPAD']:
     PARALLEL_DIRS += ['gamepad']
 
 if CONFIG['MOZ_NFC']:
     PARALLEL_DIRS += ['nfc']
 
+if CONFIG['MOZ_B2G']:
+    PARALLEL_DIRS += ['downloads']
+
 # bindings/test is here, because it needs to build after bindings/, and
 # we build subdirectories before ourselves.
 TEST_DIRS += [
     'tests',
     'imptests',
     'bindings/test',
 ]
 
--- a/dom/network/tests/marionette/test_mobile_voice_state.js
+++ b/dom/network/tests/marionette/test_mobile_voice_state.js
@@ -89,17 +89,17 @@ function testSignalStrength() {
 function testUnregistered() {
   setEmulatorVoiceState("unregistered");
 
   connection.addEventListener("voicechange", function onvoicechange() {
     connection.removeEventListener("voicechange", onvoicechange);
 
     is(connection.voice.connected, false);
     is(connection.voice.state, "notSearching");
-    is(connection.voice.emergencyCallsOnly, false);
+    is(connection.voice.emergencyCallsOnly, true);
     is(connection.voice.roaming, false);
     is(connection.voice.cell, null);
     is(connection.voice.signalStrength, null);
     is(connection.voice.relSignalStrength, null);
 
     testSearching();
   });
 }
@@ -107,17 +107,17 @@ function testUnregistered() {
 function testSearching() {
   setEmulatorVoiceState("searching");
 
   connection.addEventListener("voicechange", function onvoicechange() {
     connection.removeEventListener("voicechange", onvoicechange);
 
     is(connection.voice.connected, false);
     is(connection.voice.state, "searching");
-    is(connection.voice.emergencyCallsOnly, false);
+    is(connection.voice.emergencyCallsOnly, true);
     is(connection.voice.roaming, false);
     is(connection.voice.cell, null);
     is(connection.voice.signalStrength, null);
     is(connection.voice.relSignalStrength, null);
 
     testDenied();
   });
 }
@@ -125,17 +125,17 @@ function testSearching() {
 function testDenied() {
   setEmulatorVoiceState("denied");
 
   connection.addEventListener("voicechange", function onvoicechange() {
     connection.removeEventListener("voicechange", onvoicechange);
 
     is(connection.voice.connected, false);
     is(connection.voice.state, "denied");
-    is(connection.voice.emergencyCallsOnly, false);
+    is(connection.voice.emergencyCallsOnly, true);
     is(connection.voice.roaming, false);
     is(connection.voice.cell, null);
     is(connection.voice.signalStrength, null);
     is(connection.voice.relSignalStrength, null);
 
     testRoaming();
   });
 }
--- a/dom/system/gonk/RILContentHelper.js
+++ b/dom/system/gonk/RILContentHelper.js
@@ -1097,16 +1097,17 @@ RILContentHelper.prototype = {
   updateContact: function updateContact(clientId, window, contactType, contact, pin2) {
     if (window == null) {
       throw Components.Exception("Can't get window object",
                                   Cr.NS_ERROR_UNEXPECTED);
     }
 
     let request = Services.DOMRequest.createRequest(window);
     let requestId = this.getRequestId(request);
+    this._windowsMap[requestId] = window;
 
     // Parsing nsDOMContact to Icc Contact format
     let iccContact = {};
 
     if (Array.isArray(contact.name) && contact.name[0]) {
       iccContact.alphaId = contact.name[0];
     }
 
@@ -1723,17 +1724,17 @@ RILContentHelper.prototype = {
         break;
       case "RIL:IccExchangeAPDU":
         this.handleIccExchangeAPDU(data);
         break;
       case "RIL:ReadIccContacts":
         this.handleReadIccContacts(data);
         break;
       case "RIL:UpdateIccContact":
-        this.handleSimpleRequest(data.requestId, data.errorMsg, null);
+        this.handleUpdateIccContact(data);
         break;
       case "RIL:DataError":
         this.updateConnectionInfo(data, this.rilContexts[clientId].dataConnectionInfo);
         this._deliverEvent(clientId, "_mobileConnectionListeners", "notifyDataError",
                            [data.errorMsg]);
         break;
       case "RIL:GetCallForwardingOptions":
         this.handleGetCallForwardingOptions(data);
@@ -1889,16 +1890,42 @@ RILContentHelper.prototype = {
       let contact = new window.mozContact(prop);
       contact.id = c.contactId;
       return contact;
     });
 
     this.fireRequestSuccess(message.requestId, result);
   },
 
+  handleUpdateIccContact: function handleUpdateIccContact(message) {
+    if (message.errorMsg) {
+      this.fireRequestError(message.requestId, message.errorMsg);
+      return;
+    }
+
+    let window = this._windowsMap[message.requestId];
+    delete this._windowsMap[message.requestId];
+    let iccContact = message.contact;
+    let prop = {name: [iccContact.alphaId], tel: [{value: iccContact.number}]};
+    if (iccContact.email) {
+      prop.email = [{value: iccContact.email}];
+    }
+
+    // ANR - Additional Number
+    let anrLen = iccContact.anr ? iccContact.anr.length : 0;
+    for (let i = 0; i < anrLen; i++) {
+      prop.tel.push({value: iccContact.anr[i]});
+    }
+
+    let contact = new window.mozContact(prop);
+    contact.id = iccContact.contactId;
+
+    this.fireRequestSuccess(message.requestId, contact);
+  },
+
   handleVoicemailNotification: function handleVoicemailNotification(clientId,
                                                                     message) {
     let changed = false;
     if (!this.voicemailStatuses[clientId]) {
       this.voicemailStatuses[clientId] = new VoicemailStatus(clientId);
     }
 
     let voicemailStatus = this.voicemailStatuses[clientId];
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -74,17 +74,16 @@ const MMI_MAX_LENGTH_SHORT_CODE = 2;
 
 const MMI_END_OF_USSD = "#";
 
 let RILQUIRKS_CALLSTATE_EXTRA_UINT32 = libcutils.property_get("ro.moz.ril.callstate_extra_int", "false") === "true";
 // This may change at runtime since in RIL v6 and later, we get the version
 // number via the UNSOLICITED_RIL_CONNECTED parcel.
 let RILQUIRKS_V5_LEGACY = libcutils.property_get("ro.moz.ril.v5_legacy", "true") === "true";
 let RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL = libcutils.property_get("ro.moz.ril.dial_emergency_call", "false") === "true";
-let RILQUIRKS_MODEM_DEFAULTS_TO_EMERGENCY_MODE = libcutils.property_get("ro.moz.ril.emergency_by_default", "false") === "true";
 let RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS = libcutils.property_get("ro.moz.ril.simstate_extra_field", "false") === "true";
 // Needed for call-waiting on Peak device
 let RILQUIRKS_EXTRA_UINT32_2ND_CALL = libcutils.property_get("ro.moz.ril.extra_int_2nd_call", "false") == "true";
 // On the emulator we support querying the number of lock retries
 let RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT = libcutils.property_get("ro.moz.ril.query_icc_count", "false") == "true";
 
 // Ril quirk to Send STK Profile Download
 let RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD = libcutils.property_get("ro.moz.ril.send_stk_profile_dl", "false") == "true";
@@ -898,40 +897,43 @@ let RIL = {
    *
    * @param contactType   "adn" or "fdn".
    * @param contact       The contact will be updated.
    * @param pin2          PIN2 is required for updating FDN.
    * @param requestId     Request id from RadioInterfaceLayer.
    */
   updateICCContact: function updateICCContact(options) {
     let onsuccess = function onsuccess() {
+      let recordIndex =
+        contact.pbrIndex * ICC_MAX_LINEAR_FIXED_RECORDS + contact.recordId;
+      contact.contactId = this.iccInfo.iccid + recordIndex;
       // Reuse 'options' to get 'requestId' and 'contactType'.
       RIL.sendChromeMessage(options);
     }.bind(this);
 
     let onerror = function onerror(errorMsg) {
       options.errorMsg = errorMsg;
       RIL.sendChromeMessage(options);
     }.bind(this);
 
     if (!this.appType || !options.contact) {
       onerror(GECKO_ERROR_REQUEST_NOT_SUPPORTED);
       return;
     }
 
     let contact = options.contact;
     let iccid = RIL.iccInfo.iccid;
+    let isValidRecordId = false;
     if (typeof contact.contactId === "string" &&
         contact.contactId.startsWith(iccid)) {
       let recordIndex = contact.contactId.substring(iccid.length);
       contact.pbrIndex = Math.floor(recordIndex / ICC_MAX_LINEAR_FIXED_RECORDS);
       contact.recordId = recordIndex % ICC_MAX_LINEAR_FIXED_RECORDS;
-    }
-
-    let isValidRecordId = contact.recordId > 0 && contact.recordId < 0xff;
+      isValidRecordId = contact.recordId > 0 && contact.recordId < 0xff;
+    }
 
     if (DEBUG) {
       debug("Update ICC Contact " + JSON.stringify(contact));
     }
 
     // If contact has 'recordId' property, updates corresponding record.
     // If not, inserts the contact into a free record.
     if (isValidRecordId) {
@@ -3290,22 +3292,17 @@ let RIL = {
     if (curState.regState === undefined || curState.regState !== regState) {
       changed = true;
       curState.regState = regState;
 
       curState.state = NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[regState];
       curState.connected = regState == NETWORK_CREG_STATE_REGISTERED_HOME ||
                            regState == NETWORK_CREG_STATE_REGISTERED_ROAMING;
       curState.roaming = regState == NETWORK_CREG_STATE_REGISTERED_ROAMING;
-      curState.emergencyCallsOnly =
-        (regState >= NETWORK_CREG_STATE_NOT_SEARCHING_EMERGENCY_CALLS) &&
-        (regState <= NETWORK_CREG_STATE_UNKNOWN_EMERGENCY_CALLS);
-      if (RILQUIRKS_MODEM_DEFAULTS_TO_EMERGENCY_MODE) {
-        curState.emergencyCallsOnly = !curState.connected;
-      }
+      curState.emergencyCallsOnly = !curState.connected;
     }
 
     if (!curState.cell) {
       curState.cell = {};
     }
 
     // From TS 23.003, 0000 and 0xfffe are indicated that no valid LAI exists
     // in MS. So we still need to report the '0000' as well.
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -215,16 +215,19 @@ var interfaceNamesInGlobalScope =
     "DOMRect",
     "DOMRectList",
     "DOMRequest",
     "DOMSettableTokenList",
     "DOMStringList",
     "DOMStringMap",
     "DOMTokenList",
     "DOMTransactionEvent",
+    {name: "DOMDownload", b2g: true, pref: "dom.mozDownloads.enabled"},
+    {name: "DOMDownloadManager", b2g: true, pref: "dom.mozDownloads.enabled"},
+    {name: "DownloadEvent", b2g: true, pref: "dom.mozDownloads.enabled"},
     "DragEvent",
     "DynamicsCompressorNode",
     "Element",
     "ElementReplaceEvent",
     "ErrorEvent",
     "Event",
     "EventListenerInfo",
     "EventSource",
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DownloadEvent.webidl
@@ -0,0 +1,17 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[Constructor(DOMString type, optional DownloadEventInit eventInitDict),
+ Pref="dom.mozDownloads.enabled"]
+interface DownloadEvent : Event
+{
+  readonly attribute DOMDownload? download;
+};
+
+dictionary DownloadEventInit : EventInit
+{
+  DOMDownload? download = null;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/Downloads.webidl
@@ -0,0 +1,75 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[NavigatorProperty="mozDownloadManager",
+ JSImplementation="@mozilla.org/downloads/manager;1",
+ Pref="dom.mozDownloads.enabled"]
+interface DOMDownloadManager : EventTarget {
+  // This promise returns an array of downloads with all the current
+  // download objects.
+  Promise getDownloads();
+
+  // Removes one download from the downloads set. Returns a promise resolved
+  // with the finalized download.
+  Promise remove(DOMDownload download);
+
+  // Removes all the completed downloads from the set.
+  Promise clearAllDone();
+
+  // Fires when a new download starts.
+  attribute EventHandler ondownloadstart;
+};
+
+[JSImplementation="@mozilla.org/downloads/download;1",
+ Pref="dom.mozDownloads.enabled"]
+interface DOMDownload : EventTarget {
+  // The full size of the resource.
+  readonly attribute long totalBytes;
+
+  // The number of bytes that we have currently downloaded.
+  readonly attribute long currentBytes;
+
+  // The url of the resource.
+  readonly attribute DOMString url;
+
+  // The path in local storage where the file will end up once the download
+  // is complete.
+  readonly attribute DOMString path;
+
+  // The state of the download. Can be any of:
+  // "downloading": The resource is actively transfering.
+  // "stopped"    : No network tranfer is happening.
+  // "succeeded"  : The resource has been downloaded successfully.
+  // "finalized"  : We won't try to download this resource, but the DOM
+  //                object is still alive.
+  readonly attribute DOMString state;
+
+  // The mime type for this resource.
+  readonly attribute DOMString contentType;
+
+  // The timestamp this download started.
+  readonly attribute Date startTime;
+
+  // An opaque identifier for this download. All instances of the same
+  // download (eg. in different windows) will have the same id.
+  readonly attribute DOMString id;
+
+  // A DOM error object, that will be not null when a download is stopped
+  // because something failed.
+  readonly attribute DOMError error;
+
+  // Pauses the download.
+  Promise pause();
+
+  // Resumes the download. This resolves only once the download has
+  // succeeded.
+  Promise resume();
+
+  // This event is triggered anytime a property of the object changes:
+  // - when the transfer progresses, updating currentBytes.
+  // - when the state and/or error attributes change.
+  attribute EventHandler onstatechange;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -81,16 +81,17 @@ WEBIDL_FILES = [
     'DOMParser.webidl',
     'DOMRect.webidl',
     'DOMRectList.webidl',
     'DOMRequest.webidl',
     'DOMSettableTokenList.webidl',
     'DOMStringMap.webidl',
     'DOMTokenList.webidl',
     'DOMTransaction.webidl',
+    'Downloads.webidl',
     'DragEvent.webidl',
     'DummyBinding.webidl',
     'DynamicsCompressorNode.webidl',
     'Element.webidl',
     'Event.webidl',
     'EventHandler.webidl',
     'EventListener.webidl',
     'EventSource.webidl',
@@ -561,16 +562,17 @@ if CONFIG['ENABLE_TESTS']:
     ]
 
 GENERATED_EVENTS_WEBIDL_FILES = [
     'BlobEvent.webidl',
     'CallGroupErrorEvent.webidl',
     'DataStoreChangeEvent.webidl',
     'DeviceLightEvent.webidl',
     'DeviceProximityEvent.webidl',
+    'DownloadEvent.webidl',
     'ErrorEvent.webidl',
     'IccChangeEvent.webidl',
     'MediaStreamEvent.webidl',
     'MozContactChangeEvent.webidl',
     'MozInterAppMessageEvent.webidl',
     'MozStkCommandEvent.webidl',
     'RTCDataChannelEvent.webidl',
     'RTCPeerConnectionIceEvent.webidl',
--- a/layout/base/nsDisplayList.cpp
+++ b/layout/base/nsDisplayList.cpp
@@ -614,16 +614,22 @@ static void RecordFrameMetrics(nsIFrame*
   }
 
   nsIScrollableFrame* scrollableFrame = nullptr;
   if (aScrollFrame)
     scrollableFrame = aScrollFrame->GetScrollTargetFrame();
 
   if (scrollableFrame) {
     nsRect contentBounds = scrollableFrame->GetScrollRange();
+    if (scrollableFrame->GetScrollbarStyles().mVertical == NS_STYLE_OVERFLOW_HIDDEN) {
+      contentBounds.height = 0;
+    }
+    if (scrollableFrame->GetScrollbarStyles().mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) {
+      contentBounds.width = 0;
+    }
     contentBounds.width += scrollableFrame->GetScrollPortRect().width;
     contentBounds.height += scrollableFrame->GetScrollPortRect().height;
     metrics.mScrollableRect = CSSRect::FromAppUnits(contentBounds);
     nsPoint scrollPosition = scrollableFrame->GetScrollPosition();
     metrics.mScrollOffset = CSSPoint::FromAppUnits(scrollPosition);
   }
   else {
     nsRect contentBounds = aForFrame->GetRect();
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -301,17 +301,26 @@ this.DownloadIntegration = {
    * @return True to save the download, false otherwise.
    */
   shouldPersistDownload: function (aDownload)
   {
     // In the default implementation, we save all the downloads currently in
     // progress, as well as stopped downloads for which we retained partially
     // downloaded data.  Stopped downloads for which we don't need to track the
     // presence of a ".part" file are only retained in the browser history.
+    // On b2g, we keep a few days of history.
+#ifdef MOZ_B2G
+    let maxTime = Date.now() -
+      Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000;
+    return (aDownload.startTime > maxTime) ||
+           aDownload.hasPartialData ||
+           !aDownload.stopped;
+#else
     return aDownload.hasPartialData || !aDownload.stopped;
+#endif
   },
 
   /**
    * Returns the system downloads directory asynchronously.
    *
    * @return {Promise}
    * @resolves The downloads directory string path.
    */
--- a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
@@ -103,17 +103,17 @@ XPCOMUtils.defineLazyGetter(DownloadUIHe
  * Allows displaying prompts related to downloads.
  *
  * @param aParent
  *        The nsIDOMWindow to which prompts should be attached, or null to
  *        attach prompts to the most recently active window.
  */
 this.DownloadPrompter = function (aParent)
 {
-#ifdef MOZ_WIDGET_GONK
+#ifdef MOZ_B2G
   // On B2G there is no prompter implementation.
   this._prompter = null;
 #else
   this._prompter = Services.ww.getNewPrompter(aParent);
 #endif
 }
 
 this.DownloadPrompter.prototype = {