Bug 1014338 - Part 2: add delayed distro initialization hook. r=margaret
authorRichard Newman <rnewman@mozilla.com>
Thu, 22 May 2014 17:35:44 -0700
changeset 184460 74d77419ad614f37378cd5de697e85929fafc2f5
parent 184459 5054df38fc143e1873e5552cb475bfc00a807403
child 184561 975c221719aaf2703a096425f9e6b6f85275a4c2
push id6935
push userrnewman@mozilla.com
push dateFri, 23 May 2014 00:35:59 +0000
treeherderfx-team@74d77419ad61 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs1014338
milestone32.0a1
Bug 1014338 - Part 2: add delayed distro initialization hook. r=margaret
mobile/android/base/distribution/Distribution.java
--- a/mobile/android/base/distribution/Distribution.java
+++ b/mobile/android/base/distribution/Distribution.java
@@ -11,17 +11,19 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Scanner;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
@@ -40,23 +42,39 @@ import android.util.Log;
  */
 public final class 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;
 
+    private static Distribution instance;
+
     private final Context context;
     private final String packagePath;
     private final String prefsBranch;
 
     private volatile int state = STATE_UNKNOWN;
     private File distributionDir = null;
 
+    private final Queue<Runnable> onDistributionReady = new ConcurrentLinkedQueue<Runnable>();
+
+    /**
+     * This is a little bit of a bad singleton, because in principle a Distribution
+     * can be created with arbitrary paths. So we only have one path to get here, and
+     * it uses the default arguments. Watch out if you're creating your own instances!
+     */
+    public static synchronized Distribution getInstance(Context context) {
+        if (instance == null) {
+            instance = new Distribution(context);
+        }
+        return instance;
+    }
+
     public static class DistributionDescriptor {
         public final boolean valid;
         public final String id;
         public final String version;    // Example uses a float, but that's a crazy idea.
 
         // Default UI-visible description of the distribution.
         public final String about;
 
@@ -89,43 +107,46 @@ public final class Distribution {
 
             this.localizedAbout = Collections.unmodifiableMap(loc);
             this.valid = (null != this.id) &&
                          (null != this.version) &&
                          (null != this.about);
         }
     }
 
+    private static void init(final Distribution distribution) {
+        // Read/write preferences and files on the background thread.
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                boolean distributionSet = distribution.doInit();
+                if (distributionSet) {
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
+                }
+            }
+        });
+    }
+
     /**
      * Initializes distribution if it hasn't already been initialized. Sends
      * messages to Gecko as appropriate.
      *
      * @param packagePath where to look for the distribution directory.
      */
     @RobocopTarget
     public static void init(final Context context, final String packagePath, final String prefsPath) {
-        // Read/write preferences and files on the background thread.
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                Distribution dist = new Distribution(context, packagePath, prefsPath);
-                boolean distributionSet = dist.doInit();
-                if (distributionSet) {
-                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
-                }
-            }
-        });
+        init(new Distribution(context, packagePath, prefsPath));
     }
 
     /**
      * Use <code>Context.getPackageResourcePath</code> to find an implicit
-     * package path.
+     * package path. Reuses the existing Distribution if one exists.
      */
     public static void init(final Context context) {
-        Distribution.init(context, context.getPackageResourcePath(), null);
+        Distribution.init(Distribution.getInstance(context));
     }
 
     /**
      * Returns parsed contents of bookmarks.json.
      * This method should only be called from a background thread.
      */
     public static JSONArray getBookmarks(final Context context) {
         Distribution dist = new Distribution(context);
@@ -236,37 +257,53 @@ public final class Distribution {
             settings = GeckoSharedPrefs.forApp(context);
         } else {
             settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
         }
 
         String keyName = context.getPackageName() + ".distribution_state";
         this.state = settings.getInt(keyName, STATE_UNKNOWN);
         if (this.state == STATE_NONE) {
+            runReadyQueue();
             return false;
         }
 
         // We've done the work once; don't do it again.
         if (this.state == STATE_SET) {
             // Note that we don't compute the distribution directory.
             // Call `ensureDistributionDir` if you need it.
+            runReadyQueue();
             return true;
         }
 
         // We try the APK, then the system directory.
         final boolean distributionSet =
                 checkAPKDistribution() ||
                 checkSystemDistribution();
 
         this.state = distributionSet ? STATE_SET : STATE_NONE;
         settings.edit().putInt(keyName, this.state).commit();
+
+        runReadyQueue();
         return distributionSet;
     }
 
     /**
+     * Execute tasks that wanted to run when we were done loading
+     * the distribution. These tasks are expected to call {@link #exists()}
+     * to find out whether there's a distribution or not.
+     */
+    private void runReadyQueue() {
+        Runnable task;
+        while ((task = onDistributionReady.poll()) != null) {
+            ThreadUtils.postToBackgroundThread(task);
+        }
+    }
+
+    /**
      * @return true if we copied files out of the APK. Sets distributionDir in that case.
      */
     private boolean checkAPKDistribution() {
         try {
             // First, try copying distribution files out of the APK.
             if (copyFiles()) {
                 // We always copy to the data dir, and we only copy files from
                 // a 'distribution' subdirectory. Track our dist dir now that
@@ -421,9 +458,32 @@ public final class Distribution {
 
     private String getDataDir() {
         return context.getApplicationInfo().dataDir;
     }
 
     private File getSystemDistributionDir() {
         return new File("/system/" + context.getPackageName() + "/distribution");
     }
+
+    /**
+     * The provided <code>Runnable</code> will be executed after the distribution
+     * is ready, or discarded if the distribution has already been processed.
+     *
+     * Each <code>Runnable</code> will be executed on the background thread.
+     */
+    public void addOnDistributionReadyCallback(Runnable runnable) {
+        if (state == STATE_UNKNOWN) {
+            this.onDistributionReady.add(runnable);
+        } else {
+            // If we're already initialized, just queue up the runnable.
+            ThreadUtils.postToBackgroundThread(runnable);
+        }
+    }
+
+    /**
+     * A safe way for callers to determine if this Distribution instance
+     * represents a real live distribution.
+     */
+    public boolean exists() {
+        return state == STATE_SET;
+    }
 }