Merge mozilla-central and fx-team
authorEd Morley <emorley@mozilla.com>
Wed, 11 Jun 2014 10:10:34 +0100
changeset 208953 62f27467dcab24dc7cddd08a0f8434b0a1ee458e
parent 208811 18c21532848a1673c6c3fe57e710fbf6e9cb35e1 (current diff)
parent 208952 b7256028974817de5a9fcbce0c137ec0229e1fd6 (diff)
child 208954 20f9b816ebcdd8d7aaa25556cd9a8f9bdd6cb15e
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central and fx-team
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -1792,17 +1792,17 @@ Scope.prototype = {
   _displayScope: function(aName, aTargetClassName, aTitleClassName = "") {
     let document = this.document;
 
     let element = this._target = document.createElement("vbox");
     element.id = this._idString;
     element.className = aTargetClassName;
 
     let arrow = this._arrow = document.createElement("hbox");
-    arrow.className = "arrow";
+    arrow.className = "arrow theme-twisty";
 
     let name = this._name = document.createElement("label");
     name.className = "plain name";
     name.setAttribute("value", aName);
 
     let title = this._title = document.createElement("hbox");
     title.className = "title " + aTitleClassName;
     title.setAttribute("align", "center");
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -367,16 +367,17 @@
 @BINPATH@/browser/components/BrowserDownloads.manifest
 @BINPATH@/browser/components/DownloadsStartup.js
 @BINPATH@/browser/components/DownloadsUI.js
 @BINPATH@/browser/components/BrowserPlaces.manifest
 @BINPATH@/browser/components/devtools-clhandler.manifest
 @BINPATH@/browser/components/devtools-clhandler.js
 @BINPATH@/browser/components/Experiments.manifest
 @BINPATH@/browser/components/ExperimentsService.js
+@BINPATH@/browser/components/translation.manifest
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 @BINPATH@/components/BrowserPageThumbs.manifest
 @BINPATH@/components/crashmonitor.manifest
 @BINPATH@/components/nsCrashMonitor.js
 @BINPATH@/components/SiteSpecificUserAgent.js
 @BINPATH@/components/SiteSpecificUserAgent.manifest
 @BINPATH@/components/toolkitsearch.manifest
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -277,16 +277,20 @@ div.CodeMirror span.eval-text {
 .theme-twisty:-moz-focusring {
   outline-style: none;
 }
 
 .theme-twisty[open] {
   background-position: -42px -14px;
 }
 
+.theme-twisty[invisible] {
+  visibility: hidden;
+}
+
 .theme-checkbox {
   display: inline-block;
   border: 0;
   padding: 0;
   outline: none;
   background-position: -28px 0;
 }
 
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -277,16 +277,20 @@ div.CodeMirror span.eval-text {
 .theme-twisty:-moz-focusring {
   outline-style: none;
 }
 
 .theme-twisty[open] {
   background-position: -14px -14px;
 }
 
+.theme-twisty[invisible] {
+  visibility: hidden;
+}
+
 /* Use white twisty when next to a selected item in markup view */
 .theme-selected ~ .theme-twisty {
   background-position: -28px -14px;
 }
 
 .theme-selected ~ .theme-twisty[open] {
   background-position: -42px -14px;
 }
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -625,18 +625,22 @@
 }
 
 /* Generic variables *and* properties traits */
 
 .variable-or-property:focus > .title > label {
   color: inherit !important;
 }
 
-.variable-or-property > .title > .arrow {
-  -moz-margin-start: 3px;
+.variables-view-container .theme-twisty {
+  margin: 2px;
+}
+
+.variable-or-property > .title > .theme-twisty {
+  -moz-margin-start: 5px;
 }
 
 .variable-or-property:not([untitled]) > .variables-view-element-details {
   -moz-margin-start: 7px;
 }
 
 /* Traits applied when variables or properties are changed or overridden */
 
@@ -887,32 +891,16 @@
   min-height: 24px;
 }
 
 .variable-or-property[unmatched] {
   border: none;
   margin: 0;
 }
 
-/* Expand/collapse arrow */
-
-.arrow {
-  -moz-appearance: treetwisty;
-  width: 20px;
-  height: 20px;
-}
-
-.arrow[open] {
-  -moz-appearance: treetwistyopen;
-}
-
-.arrow[invisible] {
-  visibility: hidden;
-}
-
 /* Canvas graphs */
 
 .graph-widget-canvas {
   width: 100%;
   height: 100%;
 }
 
 .graph-widget-canvas[input=hovering-background] {
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -1,19 +1,9 @@
 /* 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/. */
 
 %include ../../shared/devtools/widgets.inc.css
 
 .breadcrumbs-widget-item:-moz-focusring > .button-box {
   border-width: 0;
-}
-
-.arrow {
-  -moz-appearance: none;
-  background: url("chrome://global/skin/tree/twisty-clsd.png") center center no-repeat;
-}
-
-.arrow[open] {
-  -moz-appearance: none;
-  background-image: url("chrome://global/skin/tree/twisty-open.png");
-}
+}
\ No newline at end of file
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -301,62 +301,60 @@ this.DOMApplicationRegistry = {
           !isAbsoluteURI(redirect.to)) {
         res.push(redirect);
       }
     }
     return res.length > 0 ? res : null;
   },
 
   // Registers all the activities and system messages.
-  registerAppsHandlers: function(aRunUpdate) {
+  registerAppsHandlers: Task.async(function*(aRunUpdate) {
     this.notifyAppsRegistryStart();
     let ids = [];
     for (let id in this.webapps) {
       ids.push({ id: id });
     }
     if (supportSystemMessages()) {
       this._processManifestForIds(ids, aRunUpdate);
     } else {
       // Read the CSPs and roles. If MOZ_SYS_MSG is defined this is done on
       // _processManifestForIds so as to not reading the manifests
       // twice
-      this._readManifests(ids).then((aResults) => {
-        aResults.forEach((aResult) => {
-          if (!aResult.manifest) {
-            // If we can't load the manifest, we probably have a corrupted
-            // registry. We delete the app since we can't do anything with it.
-            delete this.webapps[aResult.id];
-            return;
-          }
-          let app = this.webapps[aResult.id];
-          app.csp = aResult.manifest.csp || "";
-          app.role = aResult.manifest.role || "";
-          if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
-            app.redirects = this.sanitizeRedirects(aResult.redirects);
-          }
-        });
+      let results = yield this._readManifests(ids);
+      results.forEach((aResult) => {
+        if (!aResult.manifest) {
+          // If we can't load the manifest, we probably have a corrupted
+          // registry. We delete the app since we can't do anything with it.
+          delete this.webapps[aResult.id];
+          return;
+        }
+        let app = this.webapps[aResult.id];
+        app.csp = aResult.manifest.csp || "";
+        app.role = aResult.manifest.role || "";
+        if (app.appStatus >= Ci.nsIPrincipal.APP_STATUS_PRIVILEGED) {
+          app.redirects = this.sanitizeRedirects(aResult.redirects);
+        }
       });
 
       // Nothing else to do but notifying we're ready.
       this.notifyAppsRegistryReady();
     }
-  },
-
-  updateDataStoreForApp: function(aId) {
+  }),
+
+  updateDataStoreForApp: Task.async(function*(aId) {
     if (!this.webapps[aId]) {
       return;
     }
 
     // Create or Update the DataStore for this app
-    this._readManifests([{ id: aId }]).then((aResult) => {
-      let app = this.webapps[aId];
-      this.updateDataStore(app.localId, app.origin, app.manifestURL,
-                           aResult[0].manifest, app.appStatus);
-    });
-  },
+    let results = yield this._readManifests([{ id: aId }]);
+    let app = this.webapps[aId];
+    this.updateDataStore(app.localId, app.origin, app.manifestURL,
+                         results[0].manifest, app.appStatus);
+  }),
 
   updatePermissionsForApp: function(aId, aIsPreinstalled) {
     if (!this.webapps[aId]) {
       return;
     }
 
     // Install the permissions for this app, as if we were updating
     // to cleanup the old ones if needed.
@@ -604,20 +602,20 @@ this.DOMApplicationRegistry = {
         }
         // Need to update the persisted list of apps since
         // installPreinstalledApp() removes the ones failing to install.
         this._saveApps();
       }
 
       // DataStores must be initialized at startup.
       for (let id in this.webapps) {
-        this.updateDataStoreForApp(id);
+        yield this.updateDataStoreForApp(id);
       }
 
-      this.registerAppsHandlers(runUpdate);
+      yield this.registerAppsHandlers(runUpdate);
     }.bind(this)).then(null, Cu.reportError);
   },
 
   updateDataStore: function(aId, aOrigin, aManifestURL, aManifest, aAppStatus) {
     // Just Certified Apps can use DataStores
     let prefName = "dom.testing.datastore_enabled_for_hosted_apps";
     if (aAppStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED &&
         (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_INVALID ||
@@ -1096,17 +1094,21 @@ this.DOMApplicationRegistry = {
         this.doInstall(msg, mm);
 #endif
         break;
       }
       case "Webapps:GetSelf":
         this.getSelf(msg, mm);
         break;
       case "Webapps:Uninstall":
+#ifdef MOZ_WIDGET_ANDROID
+        Services.obs.notifyObservers(mm, "webapps-runtime-uninstall", JSON.stringify(msg));
+#else
         this.doUninstall(msg, mm);
+#endif
         break;
       case "Webapps:Launch":
         this.doLaunch(msg, mm);
         break;
       case "Webapps:CheckInstalled":
         this.checkInstalled(msg, mm);
         break;
       case "Webapps:GetInstalled":
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -411,16 +411,26 @@ abstract public class BrowserApp extends
         final ContentValues values = new ContentValues();
         values.put(ReadingListItems.URL, message.optString("url"));
         values.put(ReadingListItems.TITLE, message.optString("title"));
         values.put(ReadingListItems.LENGTH, message.optInt("length"));
         values.put(ReadingListItems.EXCERPT, message.optString("excerpt"));
         return values;
     }
 
+    void handleReaderRemoved(final String url) {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                BrowserDB.removeReadingListItemWithURL(getContentResolver(), url);
+                showToast(R.string.page_removed, Toast.LENGTH_SHORT);
+            }
+        });
+    }
+
     private void handleReaderFaviconRequest(final String url) {
         (new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public String doInBackground(Void... params) {
                 return Favicons.getFaviconURLForPageURL(url);
             }
 
             @Override
@@ -524,16 +534,17 @@ abstract public class BrowserApp extends
             "CharEncoding:Data",
             "CharEncoding:State",
             "Feedback:LastUrl",
             "Feedback:MaybeLater",
             "Feedback:OpenPlayStore",
             "Menu:Add",
             "Menu:Remove",
             "Reader:ListStatusRequest",
+            "Reader:Removed",
             "Reader:Share",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch");
 
         Distribution.init(this);
         JavaAddonManager.getInstance().init(getApplicationContext());
         mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
@@ -881,16 +892,17 @@ abstract public class BrowserApp extends
             "CharEncoding:Data",
             "CharEncoding:State",
             "Feedback:LastUrl",
             "Feedback:MaybeLater",
             "Feedback:OpenPlayStore",
             "Menu:Add",
             "Menu:Remove",
             "Reader:ListStatusRequest",
+            "Reader:Removed",
             "Reader:Share",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch");
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
@@ -1211,16 +1223,20 @@ abstract public class BrowserApp extends
                 public void run() {
                     removeAddonMenuItem(id);
                 }
             });
 
         } else if ("Reader:ListStatusRequest".equals(event)) {
             handleReaderListStatusRequest(message.getString("url"));
 
+        } else if ("Reader:Removed".equals(event)) {
+            final String url = message.getString("url");
+            handleReaderRemoved(url);
+
         } else if ("Reader:Share".equals(event)) {
             final String title = message.getString("title");
             final String url = message.getString("url");
             GeckoAppShell.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title);
 
         } else if ("Settings:Show".equals(event)) {
             final String resource =
                     message.optString(GeckoPreferences.INTENT_EXTRA_RESOURCES, null);
--- a/mobile/android/base/GlobalHistory.java
+++ b/mobile/android/base/GlobalHistory.java
@@ -101,28 +101,26 @@ class GlobalHistory {
         GeckoAppShell.notifyUriVisited(uri);
     }
 
     public void add(String uri) {
         final long start = SystemClock.uptimeMillis();
         BrowserDB.updateVisitedHistory(GeckoAppShell.getContext().getContentResolver(), uri);
         final long end = SystemClock.uptimeMillis();
         final long took = end - start;
-        Log.d(LOGTAG, "GlobalHistory.add took " + took + "msec.");
         Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
         addToGeckoOnly(uri);
     }
 
     @SuppressWarnings("static-method")
     public void update(String uri, String title) {
         final long start = SystemClock.uptimeMillis();
         BrowserDB.updateHistoryTitle(GeckoAppShell.getContext().getContentResolver(), uri, title);
         final long end = SystemClock.uptimeMillis();
         final long took = end - start;
-        Log.d(LOGTAG, "GlobalHistory.update took " + took + "msec.");
         Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
     }
 
     public void checkUriVisited(final String uri) {
         mHandler.post(new Runnable() {
             @Override
             public void run() {
                 // this runs on the same handler thread as the processing loop,
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -1,15 +1,17 @@
 /* -*- 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.home;
 
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.mozilla.gecko.EditBookmarkDialog;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ReaderModeUtils;
 import org.mozilla.gecko.Tab;
@@ -332,17 +334,26 @@ abstract class HomeFragment extends Frag
             if (mPosition > -1) {
                 BrowserDB.unpinSite(cr, mPosition);
             }
 
             BrowserDB.removeBookmarksWithURL(cr, mUrl);
             BrowserDB.removeHistoryEntry(cr, mUrl);
 
             BrowserDB.removeReadingListItemWithURL(cr, mUrl);
-            GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", mUrl);
+
+            final JSONObject json = new JSONObject();
+            try {
+                json.put("url", mUrl);
+                json.put("notify", false);
+            } catch (JSONException e) {
+                Log.e(LOGTAG, "error building JSON arguments");
+            }
+
+            GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", json.toString());
             GeckoAppShell.sendEventToGecko(e);
 
             return null;
         }
 
         @Override
         public void onPostExecute(Void result) {
             Toast.makeText(mContext, R.string.page_removed, Toast.LENGTH_SHORT).show();
--- a/mobile/android/base/webapp/EventListener.java
+++ b/mobile/android/base/webapp/EventListener.java
@@ -26,47 +26,52 @@ import org.mozilla.gecko.util.ThreadUtil
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.os.Build;
 import android.util.Log;
 
 public class EventListener implements NativeEventListener  {
 
     private static final String LOGTAG = "GeckoWebappEventListener";
 
     public void registerEvents() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Webapps:Preinstall",
             "Webapps:InstallApk",
+            "Webapps:UninstallApk",
             "Webapps:Postinstall",
             "Webapps:Open",
             "Webapps:Uninstall",
             "Webapps:GetApkVersions");
     }
 
     public void unregisterEvents() {
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "Webapps:Preinstall",
             "Webapps:InstallApk",
+            "Webapps:UninstallApk",
             "Webapps:Postinstall",
             "Webapps:Open",
             "Webapps:Uninstall",
             "Webapps:GetApkVersions");
     }
 
     @Override
     public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
         try {
             if (event.equals("Webapps:InstallApk")) {
                 installApk(GeckoAppShell.getGeckoInterface().getActivity(), message, callback);
+            } else if (event.equals("Webapps:UninstallApk")) {
+                uninstallApk(GeckoAppShell.getGeckoInterface().getActivity(), message);
             } else if (event.equals("Webapps:Postinstall")) {
                 postInstallWebapp(message.getString("apkPackageName"), message.getString("origin"));
             } else if (event.equals("Webapps:Open")) {
                 Intent intent = GeckoAppShell.getWebappIntent(message.getString("manifestURL"),
                                                               message.getString("origin"),
                                                               "", null);
                 if (intent == null) {
                     return;
@@ -188,16 +193,30 @@ public class EventListener implements Na
                         Log.e(LOGTAG, "error unregistering install receiver: ", e);
                     }
                     callback.sendError("APK installation cancelled by user");
                 }
             }
         });
     }
 
+    public static void uninstallApk(final Activity context, NativeJSObject message) {
+        String packageName = message.getString("apkPackageName");
+        Uri packageUri = Uri.parse("package:" + packageName);
+
+        Intent intent;
+        if (Build.VERSION.SDK_INT < 14) {
+            intent = new Intent(Intent.ACTION_DELETE, packageUri);
+        } else {
+            intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
+        }
+
+        context.startActivity(intent);
+    }
+
     private static final int DEFAULT_VERSION_CODE = -1;
 
     public static JSONObject getApkVersions(Activity context, String[] packageNames) {
         Set<String> packageNameSet = new HashSet<String>();
         packageNameSet.addAll(Arrays.asList(packageNames));
 
         final PackageManager pm = context.getPackageManager();
         List<ApplicationInfo> apps = pm.getInstalledApplications(0);
--- a/mobile/android/chrome/content/aboutApps.js
+++ b/mobile/android/chrome/content/aboutApps.js
@@ -42,28 +42,53 @@ function openLink(aEvent) {
     BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
   } catch (ex) {}
 }
 
 function checkForUpdates(aEvent) {
   WebappManager.checkForUpdates(true);
 }
 
+let ContextMenus = {
+  target: null,
+
+  init: function() {
+    document.addEventListener("contextmenu", this, false);
+    document.getElementById("uninstallLabel").addEventListener("click", this.uninstall.bind(this), false);
+  },
+
+  handleEvent: function(event) {
+    // store the target of context menu events so that we know which app to act on
+    this.target = event.target;
+    while (!this.target.hasAttribute("contextmenu")) {
+      this.target = this.target.parentNode;
+    }
+  },
+
+  uninstall: function() {
+    navigator.mozApps.mgmt.uninstall(this.target.app);
+
+    this.target = null;
+  }
+};
+
 function onLoad(aEvent) {
   let elmts = document.querySelectorAll("[pref]");
   for (let i = 0; i < elmts.length; i++) {
     elmts[i].addEventListener("click",  openLink,  false);
   }
 
   document.getElementById("update-item").addEventListener("click", checkForUpdates, false);
 
   navigator.mozApps.mgmt.oninstall = onInstall;
   navigator.mozApps.mgmt.onuninstall = onUninstall;
   updateList();
 
+  ContextMenus.init();
+
   // XXX - Hack to fix bug 985867 for now
   document.addEventListener("touchstart", function() { });
 }
 
 function updateList() {
   let grid = document.getElementById("appgrid");
   while (grid.lastChild) {
     grid.removeChild(grid.lastChild);
@@ -79,16 +104,17 @@ function updateList() {
 }
 
 function addApplication(aApp) {
   let list = document.getElementById("appgrid");
   let manifest = new ManifestHelper(aApp.manifest, aApp.origin);
 
   let container = document.createElement("div");
   container.className = "app list-item";
+  container.setAttribute("contextmenu", "appmenu");
   container.setAttribute("id", "app-" + aApp.origin);
   container.setAttribute("mozApp", aApp.origin);
   container.setAttribute("title", manifest.name);
 
   let img = document.createElement("img");
   img.src = manifest.biggestIconURL || DEFAULT_ICON;
   img.onerror = function() {
     // If the image failed to load, and it was not our default icon, attempt to
--- a/mobile/android/chrome/content/aboutApps.xhtml
+++ b/mobile/android/chrome/content/aboutApps.xhtml
@@ -23,16 +23,21 @@
     <meta name="viewport" content="width=device-width; user-scalable=0" />
     <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
     <link rel="stylesheet" type="text/css" href="chrome://browser/skin/aboutBase.css" media="all" />
     <link rel="stylesheet" type="text/css" href="chrome://browser/skin/aboutApps.css" media="all" />
     <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutApps.js"></script>
   </head>
 
   <body dir="&locale.dir;">
+
+    <menu type="context" id="appmenu">
+      <menuitem id="uninstallLabel" label="&aboutApps.uninstall;"></menuitem>
+    </menu>
+
     <div class="header">
       <div>&aboutApps.header;</div>
       <div id="header-button" role="button" aria-label="&aboutApps.browseMarketplace;" pref="app.marketplaceURL"/>
     </div>
 
     <div id="main-container" class="hidden">
       <div>
         <div class="spacer" id="spacer1"> </div>
--- a/mobile/android/chrome/content/aboutReader.js
+++ b/mobile/android/chrome/content/aboutReader.js
@@ -191,19 +191,19 @@ AboutReader.prototype = {
         if (args.url == this._article.url) {
           if (this._isReadingListItem != 1) {
             this._isReadingListItem = 1;
             this._updateToggleButton();
           }
         }
         break;
       }
-
       case "Reader:Remove": {
-        if (aData == this._article.url) {
+        let args = JSON.parse(aData);
+        if (args.url == this._article.url) {
           if (this._isReadingListItem != 0) {
             this._isReadingListItem = 0;
             this._updateToggleButton();
           }
         }
         break;
       }
 
@@ -311,17 +311,18 @@ AboutReader.prototype = {
           length: this._article.length,
           excerpt: this._article.excerpt
         });
       }.bind(this));
     } else {
       // In addition to removing the article from the cache (handled in
       // browser.js), sending this message will cause the toggle button to be
       // updated (handled in this file).
-      Services.obs.notifyObservers(null, "Reader:Remove", this._article.url);
+      let json = JSON.stringify({ url: this._article.url, notify: true });
+      Services.obs.notifyObservers(null, "Reader:Remove", json);
 
       UITelemetry.addEvent("unsave.1", "button", null, "reader");
     }
   },
 
   _onShare: function Reader_onShare() {
     if (!this._article)
       return;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -323,17 +323,17 @@ var BrowserApp = {
     Services.obs.addObserver(this, "Passwords:Init", false);
     Services.obs.addObserver(this, "FormHistory:Init", false);
     Services.obs.addObserver(this, "gather-telemetry", false);
     Services.obs.addObserver(this, "keyword-search", false);
     Services.obs.addObserver(this, "webapps-runtime-install", false);
     Services.obs.addObserver(this, "webapps-runtime-install-package", false);
     Services.obs.addObserver(this, "webapps-ask-install", false);
     Services.obs.addObserver(this, "webapps-launch", false);
-    Services.obs.addObserver(this, "webapps-uninstall", false);
+    Services.obs.addObserver(this, "webapps-runtime-uninstall", false);
     Services.obs.addObserver(this, "Webapps:AutoInstall", false);
     Services.obs.addObserver(this, "Webapps:Load", false);
     Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
 
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
@@ -1626,18 +1626,18 @@ var BrowserApp = {
         WebappManager.askInstall(JSON.parse(aData));
         break;
 
       case "webapps-launch": {
         WebappManager.launch(JSON.parse(aData));
         break;
       }
 
-      case "webapps-uninstall": {
-        WebappManager.uninstall(JSON.parse(aData));
+      case "webapps-runtime-uninstall": {
+        WebappManager.uninstall(JSON.parse(aData), aSubject);
         break;
       }
 
       case "Webapps:AutoInstall":
         WebappManager.autoInstall(JSON.parse(aData));
         break;
 
       case "Webapps:Load":
@@ -7315,19 +7315,30 @@ let Reader = {
           } else {
             this.parseDocumentFromURL(urlWithoutRef, handleArticle);
           }
         }.bind(this));
         break;
       }
 
       case "Reader:Remove": {
-        let url = aData;
-        this.removeArticleFromCache(url, function(success) {
-          this.log("Reader:Remove success=" + success + ", url=" + url);
+        let args = JSON.parse(aData);
+
+        if (!("url" in args)) {
+          throw new Error("Reader:Remove requires URL as an argument");
+        }
+
+        this.removeArticleFromCache(args.url, function(success) {
+          this.log("Reader:Remove success=" + success + ", url=" + args.url);
+          if (success && args.notify) {
+            sendMessageToJava({
+              type: "Reader:Removed",
+              url: args.url
+            });
+          }
         }.bind(this));
         break;
       }
 
       case "nsPref:changed": {
         if (aData.startsWith("reader.parse-on-load.")) {
           this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
         }
--- a/mobile/android/locales/en-US/chrome/aboutApps.dtd
+++ b/mobile/android/locales/en-US/chrome/aboutApps.dtd
@@ -2,10 +2,9 @@
    - 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/. -->
 
 <!ENTITY aboutApps.title2                          "Apps">
 <!ENTITY aboutApps.header                          "Your Apps">
 
 <!ENTITY aboutApps.browseMarketplace "Browse the Firefox Marketplace">
 <!ENTITY aboutApps.uninstall "Uninstall">
-<!ENTITY aboutApps.addToHomescreen "Add to Home Screen">
 <!ENTITY aboutApps.checkForUpdates "Check for Updates">
--- a/mobile/android/modules/WebappManager.jsm
+++ b/mobile/android/modules/WebappManager.jsm
@@ -203,26 +203,63 @@ this.WebappManager = {
 
     sendMessageToJava({
       type: "Webapps:Open",
       manifestURL: manifestURL,
       origin: origin
     });
   },
 
-  uninstall: function(aData) {
+  uninstall: Task.async(function*(aData, aMessageManager) {
     debug("uninstall: " + aData.manifestURL);
 
+    yield DOMApplicationRegistry.registryReady;
+
     if (this._testing) {
-      // We don't have to do anything, as the registry does all the work.
+      // Go directly to DOM.  Do not uninstall APK, do not collect $200.
+      DOMApplicationRegistry.doUninstall(aData, aMessageManager);
       return;
     }
 
-    // TODO: uninstall the APK.
-  },
+    let app = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL);
+    if (!app) {
+      throw new Error("app not found in registry");
+    }
+
+    // If the APK is installed, then _getAPKVersions will return a version
+    // for it, so we can use that function to determine its install status.
+    let apkVersions = yield this._getAPKVersions([ app.apkPackageName ]);
+    if (app.apkPackageName in apkVersions) {
+      debug("APK is installed; requesting uninstallation");
+      sendMessageToJava({
+        type: "Webapps:UninstallApk",
+        apkPackageName: app.apkPackageName,
+      });
+
+      // We don't need to call DOMApplicationRegistry.doUninstall at this point,
+      // because the APK uninstall listener will call autoUninstall once the APK
+      // is uninstalled; and if the user cancels the APK uninstallation, then we
+      // shouldn't remove the app from the registry anyway.
+
+      // But we should tell the requesting document the result of their request.
+      // TODO: tell the requesting document if uninstallation succeeds or fails
+      // by storing weak references to the message/manager pair here and then
+      // using them in autoUninstall if they're still defined when it's called;
+      // and make EventListener.uninstallApk return an error when APK uninstall
+      // fails (which it should be able to detect reliably on Android 4+),
+      // which we observe here and use to notify the requester of failure.
+    } else {
+      // The APK isn't installed, but remove the app from the registry anyway,
+      // to ensure the user can always remove an app from the registry (and thus
+      // about:apps) even if it's out of sync with installed APKs.
+      debug("APK not installed; proceeding directly to removal from registry");
+      DOMApplicationRegistry.doUninstall(aData, aMessageManager);
+    }
+
+  }),
 
   autoInstall: function(aData) {
     debug("autoInstall " + aData.manifestURL);
 
     // If the app is already installed, update the existing installation.
     // We should be able to use DOMApplicationRegistry.getAppByManifestURL,
     // but it returns a mozIApplication, while _autoUpdate needs the original
     // object from DOMApplicationRegistry.webapps in order to modify it.