Bug 893800 - Install single variant apps the first time a SIM is detected. r=fabrice
☠☠ backed out by 2aa1e16b67b0 ☠ ☠
authorCarmen Jimenez <cjc@tid.es>
Wed, 31 Jul 2013 21:34:19 +0200
changeset 160012 2a58557adc6f56202f27c9dab0fdc6bfff085601
parent 160011 8b613f4803f643d3a7b653c6f92d801189808aa0
child 160013 e69381450554e751b91dd04102a0fa3bbe07442f
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice
bugs893800
milestone26.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
Bug 893800 - Install single variant apps the first time a SIM is detected. r=fabrice
b2g/app/b2g.js
b2g/chrome/content/shell.js
browser/installer/removed-files.in
dom/apps/src/AppsUtils.jsm
dom/apps/src/OperatorApps.jsm
dom/apps/src/Webapps.jsm
dom/apps/src/moz.build
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -419,16 +419,17 @@ pref("services.push.requestTimeout", 100
 pref("services.push.udp.wakeupEnabled", true);
 // port on which UDP server socket is bound
 pref("services.push.udp.port", 2442);
 
 // NetworkStats
 #ifdef MOZ_B2G_RIL
 pref("dom.mozNetworkStats.enabled", true);
 pref("ril.cellbroadcast.disabled", false);
+pref("dom.webapps.firstRunWithSIM", false);
 #endif
 
 // WebSettings
 pref("dom.mozSettings.enabled", true);
 pref("dom.navigator-property.disable.mozSettings", false);
 pref("dom.mozPermissionSettings.enabled", true);
 
 // controls if we want camera support
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -596,16 +596,20 @@ var shell = {
 
     this.reportCrash(true);
 
     Cu.import('resource://gre/modules/Webapps.jsm');
     DOMApplicationRegistry.allAppsLaunchable = true;
 
     this.sendEvent(window, 'ContentStart');
 
+#ifdef MOZ_B2G_RIL
+    Cu.import('resource://gre/modules/OperatorApps.jsm');
+#endif
+
     content.addEventListener('load', function shell_homeLoaded() {
       content.removeEventListener('load', shell_homeLoaded);
       shell.isHomeLoaded = true;
 
 #ifdef MOZ_WIDGET_GONK
       libcutils.property_set('sys.boot_completed', '1');
 #endif
 
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -1110,16 +1110,17 @@ xpicleanup@BIN_SUFFIX@
   modules/services-sync/type_records/prefs.js
   modules/services-sync/type_records/tabs.js
   modules/services-sync/util.js
   modules/stylePanel.jsm
   modules/tabview/AllTabs.jsm
   modules/tabview/groups.jsm
   modules/tabview/utils.jsm
   modules/Webapps.jsm
+  modules/OperatorApps.jsm
   modules/WindowDraggingUtils.jsm
   #ifdef XP_WIN
     modules/WindowsJumpLists.jsm
     modules/WindowsPreviewPerTab.jsm
   #endif
   modules/XPCOMUtils.jsm
   modules/XPIProvider.jsm
   res/contenteditable.css
--- a/dom/apps/src/AppsUtils.jsm
+++ b/dom/apps/src/AppsUtils.jsm
@@ -7,16 +7,18 @@
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   return Cc["@mozilla.org/network/util;1"]
            .getService(Ci.nsINetUtil);
 });
 
 // Shared code for AppsServiceChild.jsm, Webapps.jsm and Webapps.js
 
@@ -484,16 +486,42 @@ this.AppsUtils = {
         return false;
       }
     }
 
     // Nothing failed.
     return true;
   },
 
+  // Loads a JSON file using OS.file. aFile is a string representing the path
+  // of the file to be read.
+  // Returns a Promise resolved with the json payload or rejected with
+  // OS.File.Error
+  loadJSONAsync: function(aFile) {
+    debug("_loadJSONAsync: " + aFile);
+    return Task.spawn(function() {
+      let file = yield OS.File.open(aFile, { read: true });
+      let rawData = yield file.read();
+      // Read json file into a string
+      let data;
+      try {
+        // Obtain a converter to read from a UTF-8 encoded input stream.
+        let converter = new TextDecoder();
+        data = JSON.parse(converter.decode(rawData));
+        file.close();
+      } catch (ex) {
+        debug("Error parsing JSON: " + aFile + ". Error: " + ex);
+        Cu.reportError("OperatorApps: Could not parse JSON: " +
+                       aFile + " " + ex + "\n" + ex.stack);
+        throw ex;
+      }
+      throw new Task.Result(data);
+    });
+  },
+
   // Returns the MD5 hash of a string.
   computeHash: function(aString) {
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                       .createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
     let result = {};
     // Data is an array of bytes.
     let data = converter.convertToByteArray(aString, result);
new file mode 100644
--- /dev/null
+++ b/dom/apps/src/OperatorApps.jsm
@@ -0,0 +1,262 @@
+/* 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 Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+this.EXPORTED_SYMBOLS = ["OperatorAppsRegistry"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Webapps.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+#ifdef MOZ_B2G_RIL
+XPCOMUtils.defineLazyServiceGetter(this, "iccProvider",
+                                   "@mozilla.org/ril/content-helper;1",
+                                   "nsIIccProvider");
+
+XPCOMUtils.defineLazyServiceGetter(this, "mobileConnection",
+                                   "@mozilla.org/ril/content-helper;1",
+                                   "nsIMobileConnectionProvider");
+#endif
+
+function debug(aMsg) {
+  //dump("-*-*- OperatorApps.jsm : " + aMsg + "\n");
+}
+
+const DIRECTORY_NAME = "webappsDir";
+
+// The files will be stored on DIRECTORY_NAME + "/" + SINGLE_VARIANT_SOURCE_DIR
+// SINGLE_VARIANT_CONF_FILE will be stored on SINGLE_VARIANT_SOURCE_DIR
+// Apps will be stored on a app per directory basis, hanging from
+// SINGLE_VARIANT_SOURCE_DIR
+const SINGLE_VARIANT_SOURCE_DIR = "svoperapps";
+const SINGLE_VARIANT_CONF_FILE  = "singlevariantconf.json";
+const PREF_FIRST_RUN_WITH_SIM   = "dom.webapps.firstRunWithSIM";
+const METADATA                  = "metadata.json";
+const UPDATEMANIFEST            = "update.webapp";
+const MANIFEST                  = "manifest.webapp";
+const APPLICATION_ZIP           = "application.zip";
+
+function isFirstRunWithSIM() {
+  try {
+    if (Services.prefs.prefHasUserValue(PREF_FIRST_RUN_WITH_SIM)) {
+      return Services.prefs.getBoolPref(PREF_FIRST_RUN_WITH_SIM);
+    }
+  } catch(e) {
+    debug ("Error getting pref. " + e);
+  }
+  return true;
+}
+
+#ifdef MOZ_B2G_RIL
+let iccListener = {
+  notifyStkCommand: function() {},
+
+  notifyStkSessionEnd: function() {},
+
+  notifyIccCardLockError: function() {},
+
+  notifyCardStateChange: function() {},
+
+  notifyIccInfoChanged: function() {
+    let iccInfo = iccProvider.iccInfo;
+    if (iccInfo && iccInfo.mcc && iccInfo.mnc) {
+      debug("******* iccListener cardIccInfo MCC-MNC: " + iccInfo.mcc +
+            "-" + iccInfo.mnc);
+      iccProvider.unregisterIccMsg(this);
+      OperatorAppsRegistry._installOperatorApps(iccInfo.mcc, iccInfo.mnc);
+    }
+  }
+};
+#endif
+
+this.OperatorAppsRegistry = {
+
+  _baseDirectory: null,
+
+  init: function() {
+    debug("init");
+#ifdef MOZ_B2G_RIL
+    if (isFirstRunWithSIM()) {
+      debug("First Run with SIM");
+      let mcc = 0;
+      let mnc = 0;
+      if (mobileConnection.iccInfo && mobileConnection.iccInfo.mcc) {
+        mcc = mobileConnection.iccInfo.mcc;
+      }
+      if (mobileConnection.iccInfo && mobileConnection.iccInfo.mnc) {
+        mnc = mobileConnection.iccInfo.mnc;
+      }
+      if (mcc && mnc) {
+        this._installOperatorApps(mcc, mnc);
+      } else {
+        iccProvider.registerIccMsg(iccListener);
+      }
+    } else {
+      debug("No First Run with SIM");
+    }
+#endif
+  },
+
+  set appsDir(aDir) {
+    debug("appsDir SET: " + aDir);
+    if (aDir) {
+      this._baseDirectory = Cc["@mozilla.org/file/local;1"]
+          .createInstance(Ci.nsILocalFile);
+      this._baseDirectory.initWithPath(aDir);
+    } else {
+      this._baseDirectory = null;
+    }
+  },
+
+  get appsDir() {
+    if (!this._baseDirectory) {
+      this._baseDirectory = FileUtils.getFile(DIRECTORY_NAME,
+                                              [SINGLE_VARIANT_SOURCE_DIR]);
+    }
+    return this._baseDirectory;
+  },
+
+  eraseVariantAppsNotInList: function(aIdsApp) {
+    if (!aIdsApp || !Array.isArray(aIdsApp)) {
+      aIdsApp = [ ];
+    }
+
+    let svDir;
+    try {
+      svDir = this.appsDir.clone();
+    } catch (e) {
+      debug("eraseVariantAppsNotInList --> Error getting Dir "+
+             svDir.path + ". " + e);
+      return;
+    }
+
+    if (!svDir || !svDir.exists()) {
+      return;
+    }
+
+    let entries = svDir.directoryEntries;
+    while (entries.hasMoreElements()) {
+      let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+      if (entry.isDirectory() && aIdsApp.indexOf(entry.leafName) < 0) {
+        try{
+          entry.remove(true);
+        } catch(e) {
+          debug("Error removing [" + entry.path + "]." + e);
+        }
+      }
+    }
+  },
+
+  _launchInstall: function(isPackage, aId, aMetadata, aManifest) {
+    if (!aManifest) {
+      debug("Error: The application " + aId + " does not have a manifest");
+      return;
+    }
+
+    let appData = {
+      app: {
+        installOrigin: aMetadata.installOrigin,
+        origin: aMetadata.origin,
+        manifestURL: aMetadata.manifestURL,
+        manifestHash: AppsUtils.computeHash(JSON.stringify(aManifest))
+      },
+      appId: undefined,
+      isBrowser: false,
+      isPackage: isPackage
+    };
+
+    if (isPackage) {
+      debug("aId:" + aId + ". Installing as packaged app.");
+      let installPack = OS.Path.join(this.appsDir.path, aId, APPLICATION_ZIP);
+      OS.File.exists(installPack).then(
+        function(aExists) {
+          if (!aExists) {
+            debug("SV " + installPack.path + " file do not exists for app " +
+                  aId);
+            return;
+          }
+          appData.app.localInstallPath = installPack;
+          appData.app.updateManifest = aManifest;
+          DOMApplicationRegistry.confirmInstall(appData);
+      });
+    } else {
+      debug("aId:" + aId + ". Installing as hosted app.");
+      appData.app.manifest = aManifest;
+      DOMApplicationRegistry.confirmInstall(appData);
+    }
+  },
+
+  _installOperatorApps: function(aMcc, aMnc) {
+    Task.spawn(function() {
+      debug("Install operator apps ---> mcc:"+ aMcc + ", mnc:" + aMnc);
+      if (!isFirstRunWithSIM()) {
+        debug("Operator apps already installed.");
+        return;
+      }
+
+      let aIdsApp = yield this._getSingleVariantApps(aMcc, aMnc);
+      debug("installOperatorApps --> aIdsApp:" + JSON.stringify(aIdsApp));
+      for (let i = 0; i < aIdsApp.length; i++) {
+        let aId = aIdsApp[i];
+        let aMetadata = yield AppsUtils.loadJSONAsync(
+                           OS.Path.join(this.appsDir.path, aId, METADATA));
+        debug("metadata:" + JSON.stringify(aMetadata));
+        let isPackage = true;
+        let manifest;
+        let manifests = [UPDATEMANIFEST, MANIFEST];
+        for (let j = 0; j < manifests.length; j++) {
+          try {
+            manifest = yield AppsUtils.loadJSONAsync(
+                          OS.Path.join(this.appsDir.path, aId, manifests[j]));
+            break;
+          } catch (e) {
+            isPackage = false;
+          }
+        }
+        if (manifest) {
+          this._launchInstall(isPackage, aId, aMetadata, manifest);
+        } else {
+          debug ("Error. Neither " + UPDATEMANIFEST + " file nor " + MANIFEST +
+                 " file for " + aId + " app.");
+        }
+      }
+      this.eraseVariantAppsNotInList(aIdsApp);
+      Services.prefs.setBoolPref(PREF_FIRST_RUN_WITH_SIM, false);
+    }.bind(this)).then(null, function(aError) {
+        debug("Error: " + aError);
+    });
+  },
+
+  _getSingleVariantApps: function(aMcc, aMnc) {
+
+    function normalizeCode(aCode) {
+      let ncode = "" + aCode;
+      while (ncode.length < 3) {
+        ncode = "0" + ncode;
+      }
+      return ncode;
+    }
+
+    return Task.spawn(function () {
+      let key = normalizeCode(aMcc) + "-" + normalizeCode(aMnc);
+      let file = OS.Path.join(this.appsDir.path, SINGLE_VARIANT_CONF_FILE);
+      let aData = yield AppsUtils.loadJSONAsync(file);
+      if (!aData || !(key in aData)) {
+        return;
+      }
+      throw new Task.Result(aData[key]);
+    }.bind(this));
+  }
+};
+
+OperatorAppsRegistry.init();
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -2190,24 +2190,36 @@ this.DOMApplicationRegistry = {
     if (!aData.isPackage) {
       this.updateAppHandlers(null, app.manifest, app);
       if (aInstallSuccessCallback) {
         aInstallSuccessCallback(app.manifest);
       }
     }
 
     if (manifest.package_path) {
+      // If it is a local app then it must been installed from a local file
+      // instead of web.
+      let origPath = jsonManifest.package_path;
+      if (aData.app.localInstallPath) {
+        jsonManifest.package_path = "file://" + aData.app.localInstallPath;
+      }
       // origin for install apps is meaningless here, since it's app:// and this
       // can't be used to resolve package paths.
       manifest = new ManifestHelper(jsonManifest, app.manifestURL);
 
       this.queuedPackageDownload[app.manifestURL] = {
         manifest: manifest,
         app: appObject,
         callback: aInstallSuccessCallback
+      };
+
+      if (aData.app.localInstallPath) {
+        // if it's a local install, there's no content process so just
+        // ack the install
+        this.onInstallSuccessAck(app.manifestURL);
       }
     }
   },
 
   _nextLocalId: function() {
     let id = Services.prefs.getIntPref("dom.mozApps.maxLocalId") + 1;
     Services.prefs.setIntPref("dom.mozApps.maxLocalId", id);
     Services.prefs.savePrefFile(null);
@@ -2286,16 +2298,24 @@ this.DOMApplicationRegistry = {
     // - check the signature on the zip.
     // - extract the manifest from the zip and check it.
     // - ask confirmation to the user.
     // - add the new app to the registry.
     // If we fail at any step, we backout the previous ones and return an error.
 
     debug("downloadPackage " + JSON.stringify(aApp));
 
+    let fullPackagePath = aManifest.fullPackagePath();
+
+    // Check if it's a local file install (we've downloaded/sideloaded the
+    // package already or it did exist on the build).
+
+    let isLocalFileInstall =
+      Services.io.extractScheme(fullPackagePath) === 'file';
+
     let id = this._appIdForManifestURL(aApp.manifestURL);
     let app = this.webapps[id];
 
     let self = this;
     // Removes the directory we created, and sends an error to the DOM side.
     function cleanup(aError) {
       debug("Cleanup: " + aError);
       let dir = FileUtils.getDir("TmpD", ["webapps", id], true, true);
@@ -2377,20 +2397,27 @@ this.DOMApplicationRegistry = {
       } else if (isInstalled) {
         throw "WRONG_APP_STORE_ID";
       }
     }
 
     function download() {
       debug("About to download " + aManifest.fullPackagePath());
 
-      let requestChannel = NetUtil.newChannel(aManifest.fullPackagePath())
-                                  .QueryInterface(Ci.nsIHttpChannel);
-      requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-      if (app.packageEtag) {
+      let requestChannel;
+      if (isLocalFileInstall) {
+        requestChannel = NetUtil.newChannel(aManifest.fullPackagePath())
+                                .QueryInterface(Ci.nsIFileChannel);
+      } else {
+        requestChannel = NetUtil.newChannel(aManifest.fullPackagePath())
+                                .QueryInterface(Ci.nsIHttpChannel);
+        requestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+      }
+
+      if (app.packageEtag && !isLocalFileInstall) {
         debug("Add If-None-Match header: " + app.packageEtag);
         requestChannel.setRequestHeader("If-None-Match", app.packageEtag, false);
       }
 
       AppDownloadManager.add(aApp.manifestURL,
         {
           channel: requestChannel,
           appId: id,
@@ -2576,18 +2603,21 @@ this.DOMApplicationRegistry = {
                 // Only allow signed apps to be installed from a whitelist of
                 // domains, and require all packages installed from any of the
                 // domains on the whitelist to be signed. This is a stopgap until
                 // we have a real story for handling multiple app stores signing
                 // apps.
                 let signedAppOriginsStr =
                   Services.prefs.getCharPref(
                     "dom.mozApps.signed_apps_installable_from");
-                let isSignedAppOrigin
-                  = signedAppOriginsStr.split(",").indexOf(aApp.installOrigin) > -1;
+                // If it's a local install and it's signed then we assume
+                // the app origin is a valid signer.
+                let isSignedAppOrigin = (isSigned && isLocalFileInstall) ||
+                                         signedAppOriginsStr.split(",").
+                                               indexOf(aApp.installOrigin) > -1;
                 if (!isSigned && isSignedAppOrigin) {
                   // Packaged apps installed from these origins must be signed;
                   // if not, assume somebody stripped the signature.
                   throw "INVALID_SIGNATURE";
                 } else if (isSigned && !isSignedAppOrigin) {
                   // Other origins are *prohibited* from installing signed apps.
                   // One reason is that our app revocation mechanism requires
                   // strong cooperation from the host of the mini-manifest, which
@@ -2629,18 +2659,20 @@ this.DOMApplicationRegistry = {
                                                 aManifest._manifest)) {
                   throw "MANIFEST_MISMATCH";
                 }
 
                 if (!AppsUtils.checkInstallAllowed(manifest, aApp.installOrigin)) {
                   throw "INSTALL_FROM_DENIED";
                 }
 
-                let maxStatus = isSigned ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
-                                         : Ci.nsIPrincipal.APP_STATUS_INSTALLED;
+                // Local file installs can be privileged even without the signature.
+                let maxStatus = isSigned || isLocalFileInstall
+                                   ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
+                                   : Ci.nsIPrincipal.APP_STATUS_INSTALLED;
 
                 if (AppsUtils.getAppManifestStatus(manifest) > maxStatus) {
                   throw "INVALID_SECURITY_LEVEL";
                 }
                 app.appStatus = AppsUtils.getAppManifestStatus(manifest);
 
                 // Save the new Etag for the package.
                 if (aIsUpdate) {
--- a/dom/apps/src/moz.build
+++ b/dom/apps/src/moz.build
@@ -29,16 +29,17 @@ EXTRA_JS_MODULES += [
     'FreeSpaceWatcher.jsm',
     'OfflineCacheInstaller.jsm',
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'AppsUtils.jsm',
+    'OperatorApps.jsm',
     'Webapps.jsm',
 ]
 
 FAIL_ON_WARNINGS = True
 
 LIBXUL_LIBRARY = True
 
 LIBRARY_NAME = 'dom_apps_s'