Bug 834681 - Add support for basic distribution modifications. r=mfinkle
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Thu, 31 Jan 2013 10:28:00 -0800
changeset 130360 493805ba1c854a572f36183ca566565961304d32
parent 130359 f08e636ecdc4170e4ca59339e1150158904723b4
child 130361 9aadf7e35e705869d01dd38a810361b5105fd934
push id2323
push userbbajaj@mozilla.com
push dateMon, 01 Apr 2013 19:47:02 +0000
treeherdermozilla-beta@7712be144d91 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs834681
milestone21.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 834681 - Add support for basic distribution modifications. r=mfinkle
mobile/android/base/BrowserApp.java
mobile/android/base/Distribution.java
mobile/android/base/Makefile.in
mobile/android/chrome/content/browser.js
mobile/android/installer/package-manifest.in
toolkit/mozapps/installer/packager.mk
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -231,16 +231,18 @@ abstract public class BrowserApp extends
 
         registerEventListener("CharEncoding:Data");
         registerEventListener("CharEncoding:State");
         registerEventListener("Feedback:LastUrl");
         registerEventListener("Feedback:OpenPlayStore");
         registerEventListener("Feedback:MaybeLater");
         registerEventListener("Dex:Load");
         registerEventListener("Telemetry:Gather");
+
+        Distribution.init(this);
     }
 
     @Override
     public void onDestroy() {
         super.onDestroy();
         if (mAboutHomeContent != null)
             mAboutHomeContent.onDestroy();
         if (mBrowserToolbar != null)
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/Distribution.java
@@ -0,0 +1,92 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * ***** BEGIN LICENSE BLOCK *****
+ *
+ * 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/.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.util.GeckoBackgroundThread;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import java.lang.Exception;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public final class Distribution {
+    private static final String LOGTAG = "Distribution";
+
+    /**
+     * Initializes distribution if it hasn't already been initalized.
+     */
+    public static void init(final Activity activity) {
+        // Read/write preferences and files on the background thread.
+        GeckoBackgroundThread.getHandler().post(new Runnable() {
+            public void run() {
+                // Bail if we've already initialized the distribution.
+                SharedPreferences settings = activity.getPreferences(Activity.MODE_PRIVATE);
+                String keyName = activity.getPackageName() + ".distribution_initialized";
+                if (settings.getBoolean(keyName, false))
+                    return;
+
+                settings.edit().putBoolean(keyName, true).commit();
+
+                try {
+                    copyFiles(activity);
+                } catch (IOException e) {
+                    Log.e(LOGTAG, "Error copying distribution files", e);
+                }
+            }
+        });
+    }
+
+    /**
+     * Copies the /distribution folder out of the APK and into the app's data directory.
+     */
+    private static void copyFiles(Activity activity) throws IOException {
+        File applicationPackage = new File(activity.getPackageResourcePath());
+        ZipFile zip = new ZipFile(applicationPackage);
+
+        Enumeration<? extends ZipEntry> zipEntries = zip.entries();
+        while (zipEntries.hasMoreElements()) {
+            ZipEntry fileEntry = zipEntries.nextElement();
+            String name = fileEntry.getName();
+
+            if (!name.startsWith("distribution/"))
+                continue;
+
+            File dataDir = new File(activity.getApplicationInfo().dataDir);
+            File outFile = new File(dataDir, name);
+
+            File dir = outFile.getParentFile();
+            if (!dir.exists())
+                dir.mkdirs();
+
+            InputStream fileStream = zip.getInputStream(fileEntry);
+            OutputStream outStream = new FileOutputStream(outFile);
+
+            int b;
+            while ((b = fileStream.read()) != -1)
+                outStream.write(b);
+
+            fileStream.close();
+            outStream.close();
+            outFile.setLastModified(fileEntry.getTime());
+        }
+
+        zip.close();
+    }
+}
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -60,16 +60,17 @@ FENNEC_JAVA_FILES = \
   CameraImageResultHandler.java \
   CameraVideoResultHandler.java \
   CanvasDelegate.java \
   CheckableLinearLayout.java \
   SyncPreference.java \
   db/BrowserDB.java \
   db/LocalBrowserDB.java \
   db/DBUtils.java \
+  Distribution.java \
   DoorHanger.java \
   DoorHangerPopup.java \
   Favicons.java \
   FilePickerResultHandler.java \
   FilePickerResultHandlerSync.java \
   FindInPageBar.java \
   FlowLayout.java \
   FontSizePreference.java \
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -8103,47 +8103,35 @@ var MemoryObserver = {
   },
 };
 
 var Distribution = {
   _file: null,
 
   init: function dc_init() {
     Services.obs.addObserver(this, "Distribution:Set", false);
+    Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
+
+    // Reload the default prefs so we can observe "prefservice:after-app-defaults"
+    Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
 
     // Look for file outside the APK:
     // /data/data/org.mozilla.fennec/distribution.json
     this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
     this._file.append("distribution.json");
-    if (this._file.exists()) {
-      let channel = NetUtil.newChannel(this._file);
-      channel.contentType = "application/json";
-      NetUtil.asyncFetch(channel, function(aStream, aResult) {
-        if (!Components.isSuccessCode(aResult)) {
-          Cu.reportError("Distribution: Could not read from distribution.json file");
-          return;
-        }
-
-        let raw = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || "";
-        aStream.close();
-
-        try {
-          this.update(JSON.parse(raw));
-        } catch (ex) {
-          Cu.reportError("Distribution: Could not parse JSON: " + ex);
-        }
-      }.bind(this));
-    } 
+    this.readJSON(this._file, this.update);
   },
 
   uninit: function dc_uninit() {
     Services.obs.removeObserver(this, "Distribution:Set");
+    Services.obs.removeObserver(this, "prefservice:after-app-defaults");
   },
 
   observe: function dc_observe(aSubject, aTopic, aData) {
+    // This event is only used for campaign tracking
     if (aTopic == "Distribution:Set") {
       // Update the prefs for this session
       try {
         this.update(JSON.parse(aData));
       } catch (ex) {
         Cu.reportError("Distribution: Could not parse JSON: " + ex);
         return;
       }
@@ -8153,24 +8141,119 @@ var Distribution = {
       ostream.init(this._file, 0x02 | 0x08 | 0x20, parseInt("600", 8), ostream.DEFER_OPEN);
 
       let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
       converter.charset = "UTF-8";
 
       // Asynchronously copy the data to the file.
       let istream = converter.convertToInputStream(aData);
       NetUtil.asyncCopy(istream, ostream, function(rc) { });
+    } else if (aTopic == "prefservice:after-app-defaults") {
+      this.getPrefs();
     }
   },
 
   update: function dc_update(aData) {
     // Force the distribution preferences on the default branch
     let defaults = Services.prefs.getDefaultBranch(null);
     defaults.setCharPref("distribution.id", aData.id);
     defaults.setCharPref("distribution.version", aData.version);
+  },
+
+  getPrefs: function dc_getPrefs() {
+    // Look for preferences file outside the APK:
+    // /data/data/org.mozilla.fennec/distribution/preferences.json
+    let file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+    file.append("distribution");
+    file.append("preferences.json");
+
+    this.readJSON(file, this.applyPrefs);
+  },
+
+  applyPrefs: function dc_applyPrefs(aData) {
+    // Check for required Global preferences
+    let global = aData["Global"];
+    if (!(global && global["id"] && global["version"] && global["about"])) {
+      Cu.reportError("Distribution: missing or incomplete Global preferences");
+      return;
+    }
+
+    // Force the distribution preferences on the default branch
+    let defaults = Services.prefs.getDefaultBranch(null);
+    defaults.setCharPref("distribution.id", global["id"]);
+    defaults.setCharPref("distribution.version", global["version"]);
+
+    let locale = Services.prefs.getCharPref("general.useragent.locale");
+    let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+    aboutString.data = global["about." + locale] || global["about"];
+    defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
+
+    let prefs = aData["Preferences"];
+    for (let key in prefs) {
+      try {
+        let value = prefs[key];
+        switch (typeof value) {
+          case "boolean":
+            defaults.setBoolPref(key, value);
+            break;
+          case "number":
+            defaults.setIntPref(key, value);
+            break;
+          case "string":
+          case "undefined":
+            defaults.setCharPref(key, value);
+            break;
+        }
+      } catch (e) { /* ignore bad prefs and move on */ }
+    }
+
+    let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
+    let localizeablePrefs = aData["LocalizablePreferences"];
+    for (let key in localizeablePrefs) {
+      try {
+        let value = localizeablePrefs[key];
+        value = value.replace("%LOCALE%", locale, "g");
+        localizedString.data = "data:text/plain," + key + "=" + value;
+        defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+      } catch (e) { /* ignore bad prefs and move on */ }
+    }
+
+    let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
+    for (let key in localizeablePrefsOverrides) {
+      try {
+        let value = localizeablePrefsOverrides[key];
+        localizedString.data = "data:text/plain," + key + "=" + value;
+        defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+      } catch (e) { /* ignore bad prefs and move on */ }
+    }
+  },
+
+  // aFile is an nsIFile
+  // aCallback takes the parsed JSON object as a parameter
+  readJSON: function dc_readJSON(aFile, aCallback) {
+    if (!aFile.exists())
+      return;
+
+    let channel = NetUtil.newChannel(aFile);
+    channel.contentType = "application/json";
+    NetUtil.asyncFetch(channel, function(aStream, aResult) {
+      if (!Components.isSuccessCode(aResult)) {
+        Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
+        return;
+      }
+
+      let raw = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || "";
+      aStream.close();
+
+      try {
+        aCallback(JSON.parse(raw));
+      } catch (e) {
+        Cu.reportError("Distribution: Could not parse JSON: " + e);
+      }
+    });
   }
 };
 
 #ifdef MOZ_WEBRTC
 var WebrtcUI = {
   init: function () {
     Services.obs.addObserver(this, "getUserMedia:request", false);
   },
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -53,16 +53,17 @@
 @BINPATH@/AndroidManifest.xml
 @BINPATH@/resources.arsc
 @BINPATH@/package-name.txt
 @BINPATH@/classes.dex
 @BINPATH@/res/drawable
 @BINPATH@/res/drawable-hdpi
 @BINPATH@/res/layout
 @BINPATH@/recommended-addons.json
+@BINPATH@/distribution/*
 
 [browser]
 ; [Base Browser Files]
 #ifndef XP_UNIX
 @BINPATH@/@MOZ_APP_NAME@.exe
 #else
 @BINPATH@/@MOZ_APP_NAME@-bin
 @BINPATH@/@MOZ_APP_NAME@
--- a/toolkit/mozapps/installer/packager.mk
+++ b/toolkit/mozapps/installer/packager.mk
@@ -289,16 +289,17 @@ DIST_FILES += \
   platform.ini \
   greprefs.js \
   browserconfig.properties \
   blocklist.xml \
   chrome.manifest \
   update.locale \
   removed-files \
   recommended-addons.json \
+  distribution \
   $(NULL)
 
 ifdef MOZ_ENABLE_SZIP
 SZIP_LIBRARIES = \
   libxul.so \
   $(NULL)
 endif