Bug 836838 - Avoid race condition in distribution initialization. r=bnicholson,mfinkle
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Fri, 01 Feb 2013 15:45:33 -0800
changeset 130518 3ef8b1c938824d3ed3e8a20f70baf0683cc52305
parent 130517 ccf2e70a3e52b8b3f5a782e6bb3acbb758e172f1
child 130519 bfb1cc4ca87fa8362735eff7a6ab8833c5693000
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)
reviewersbnicholson, mfinkle
bugs836838
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 836838 - Avoid race condition in distribution initialization. r=bnicholson,mfinkle
mobile/android/base/Distribution.java
mobile/android/base/ReferrerReceiver.java
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/Distribution.java
+++ b/mobile/android/base/Distribution.java
@@ -16,63 +16,83 @@ 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";
+    private static final String LOGTAG = "GeckoDistribution";
+
+    private static final int STATE_UNKNOWN = 0;
+    private static final int STATE_NONE = 1;
+    private static final int STATE_SET = 2;
 
     /**
      * 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))
+                String keyName = activity.getPackageName() + ".distribution_state";
+                int state = settings.getInt(keyName, STATE_UNKNOWN);
+                if (state == STATE_NONE)
                     return;
 
-                settings.edit().putBoolean(keyName, true).commit();
+                // Send a message to Gecko if we've set a distribution.
+                if (state == STATE_SET) {
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", null));
+                    return;
+                }
 
+                boolean distributionSet = false;
                 try {
-                    copyFiles(activity);
+                    distributionSet = copyFiles(activity);
                 } catch (IOException e) {
                     Log.e(LOGTAG, "Error copying distribution files", e);
                 }
+
+                if (distributionSet) {
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", null));
+                    settings.edit().putInt(keyName, STATE_SET).commit();
+                } else {
+                    settings.edit().putInt(keyName, STATE_NONE).commit();
+                }
             }
         });
     }
 
     /**
      * Copies the /distribution folder out of the APK and into the app's data directory.
+     * Returns true if distribution files were found and copied.
      */
-    private static void copyFiles(Activity activity) throws IOException {
+    private static boolean copyFiles(Activity activity) throws IOException {
         File applicationPackage = new File(activity.getPackageResourcePath());
         ZipFile zip = new ZipFile(applicationPackage);
 
+        boolean distributionSet = false;
         Enumeration<? extends ZipEntry> zipEntries = zip.entries();
         while (zipEntries.hasMoreElements()) {
             ZipEntry fileEntry = zipEntries.nextElement();
             String name = fileEntry.getName();
 
             if (!name.startsWith("distribution/"))
                 continue;
 
+            distributionSet = true;
+
             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);
@@ -83,10 +103,12 @@ public final class Distribution {
                 outStream.write(b);
 
             fileStream.close();
             outStream.close();
             outFile.setLastModified(fileEntry.getTime());
         }
 
         zip.close();
+
+        return distributionSet;
     }
 }
--- a/mobile/android/base/ReferrerReceiver.java
+++ b/mobile/android/base/ReferrerReceiver.java
@@ -46,17 +46,17 @@ public class ReferrerReceiver
 
             if (source != null && UTM_SOURCE.equals(source) && campaign != null) {
                 try {
                     JSONObject data = new JSONObject();
                     data.put("id", "playstore");
                     data.put("version", campaign);
 
                     // Try to make sure the prefs are written as a group
-                    GeckoEvent event = GeckoEvent.createBroadcastEvent("Distribution:Set", data.toString());
+                    GeckoEvent event = GeckoEvent.createBroadcastEvent("Campaign:Set", data.toString());
                     GeckoAppShell.sendEventToGecko(event);
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "Error setting distribution", e);
                 }
             }
         }
     }
 }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -8131,55 +8131,63 @@ 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);
+    Services.obs.addObserver(this, "Campaign:Set", false);
 
     // 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");
     this.readJSON(this._file, this.update);
   },
 
   uninit: function dc_uninit() {
     Services.obs.removeObserver(this, "Distribution:Set");
     Services.obs.removeObserver(this, "prefservice:after-app-defaults");
+    Services.obs.removeObserver(this, "Campaign:Set");
   },
 
   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;
-      }
-
-      // Save the data for the later sessions
-      let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
-      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();
+    switch (aTopic) {
+      case "Distribution:Set":
+        // Reload the default prefs so we can observe "prefservice:after-app-defaults"
+        Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
+        break;
+
+      case "prefservice:after-app-defaults":
+        this.getPrefs();
+        break;
+
+      case "Campaign:Set": {
+        // Update the prefs for this session
+        try {
+          this.update(JSON.parse(aData));
+        } catch (ex) {
+          Cu.reportError("Distribution: Could not parse JSON: " + ex);
+          return;
+        }
+
+        // Save the data for the later sessions
+        let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+        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) { });
+        break;
+      }
     }
   },
 
   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);