Bug 707123 - Part 1: refactor GeckoProfileDirectories out of GeckoProfile. r=nalexander
authorRichard Newman <rnewman@mozilla.com>
Mon, 03 Mar 2014 15:27:42 -0800
changeset 171688 b7e8c61b4d2e19bf8a216a390c08012f02a34e9c
parent 171687 c9a2edd50450678d26fa014081d26d380b329aaa
child 171689 bac0e16887b6845768697f35cdcd73d0175ea0f8
push id26336
push usercbook@mozilla.com
push dateTue, 04 Mar 2014 13:27:11 +0000
treeherdermozilla-central@6c1d255f9701 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs707123
milestone30.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 707123 - Part 1: refactor GeckoProfileDirectories out of GeckoProfile. r=nalexander
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoProfile.java
mobile/android/base/GeckoProfileDirectories.java
mobile/android/base/moz.build
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -9,16 +9,17 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.net.URLEncoder;
 import java.util.EnumSet;
 import java.util.Vector;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
@@ -2610,19 +2611,18 @@ abstract public class BrowserApp extends
     public void onEditSuggestion(String suggestion) {
         mBrowserToolbar.onEditSuggestion(suggestion);
     }
 
     @Override
     public int getLayout() { return R.layout.gecko_app; }
 
     @Override
-    protected String getDefaultProfileName() {
-        String profile = GeckoProfile.findDefaultProfile(this);
-        return (profile != null ? profile : GeckoProfile.DEFAULT_PROFILE);
+    protected String getDefaultProfileName() throws NoMozillaDirectoryException {
+        return GeckoProfile.getDefaultProfileName(this);
     }
 
     /**
      * Launch UI that lets the user update Firefox.
      *
      * This depends on the current channel: Release and Beta both direct to the
      * Google Play Store.  If updating is enabled, Aurora, Nightly, and custom
      * builds open about:, which provides an update interface.
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -25,16 +25,17 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.background.announcements.AnnouncementsBroadcastService;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PluginLayer;
 import org.mozilla.gecko.health.HealthRecorder;
@@ -203,17 +204,17 @@ public abstract class GeckoApp
     private volatile HealthRecorder mHealthRecorder = null;
 
     private int mSignalStrenth;
     private PhoneStateListener mPhoneStateListener = null;
     private boolean mShouldReportGeoData;
 
     abstract public int getLayout();
     abstract public boolean hasTabsSideBar();
-    abstract protected String getDefaultProfileName();
+    abstract protected String getDefaultProfileName() throws NoMozillaDirectoryException;
 
     private static final String RESTARTER_ACTION = "org.mozilla.gecko.restart";
     private static final String RESTARTER_CLASS = "org.mozilla.gecko.Restarter";
 
     @SuppressWarnings("serial")
     class SessionRestoreException extends Exception {
         public SessionRestoreException(Exception e) {
             super(e);
@@ -1194,17 +1195,25 @@ public abstract class GeckoApp
 
                 if (args.contains("-profile")) {
                     Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
                     Matcher m = p.matcher(args);
                     if (m.find()) {
                         profilePath =  m.group(1);
                     }
                     if (profileName == null) {
-                        profileName = getDefaultProfileName();
+                        try {
+                            profileName = getDefaultProfileName();
+                        } catch (NoMozillaDirectoryException e) {
+                            Log.wtf(LOGTAG, "Unable to fetch default profile name!", e);
+                            // There's nothing at all we can do now. If the Mozilla directory
+                            // didn't exist, then we're screwed.
+                            // Crash here so we can fix the bug.
+                            throw new RuntimeException(e);
+                        }
                         if (profileName == null)
                             profileName = GeckoProfile.DEFAULT_PROFILE;
                     }
                     GeckoProfile.sIsUsingCustomProfile = true;
                 }
 
                 if (profileName != null || profilePath != null) {
                     mProfile = GeckoProfile.get(this, profileName, profilePath);
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.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;
 
+import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
+import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
 import org.mozilla.gecko.util.INIParser;
 import org.mozilla.gecko.util.INISection;
 
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.File;
@@ -19,75 +21,81 @@ import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.nio.charset.Charset;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
 
 public final class GeckoProfile {
     private static final String LOGTAG = "GeckoProfile";
+
     // Used to "lock" the guest profile, so that we'll always restart in it
     private static final String LOCK_FILE_NAME = ".active_lock";
     public static final String DEFAULT_PROFILE = "default";
 
     private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
     private static String sDefaultProfileName = null;
 
+    public static boolean sIsUsingCustomProfile = false;
     private final String mName;
-    private File mProfileDir;
-    public static boolean sIsUsingCustomProfile = false;
+    private final File mMozillaDir;
+    private File mProfileDir;             // Not final because this is lazily computed.
 
     // Constants to cache whether or not a profile is "locked".
     private enum LockState {
         LOCKED,
         UNLOCKED,
         UNDEFINED
     };
+
     // Caches whether or not a profile is "locked". Only used by the guest profile to determine if it should
     // be reused or deleted on startup
     private LockState mLocked = LockState.UNDEFINED;
 
-    // Caches the guest profile dir
-    private static File mGuestDir = null;
+    // Caches the guest profile dir.
+    private static File sGuestDir = null;
+    private static GeckoProfile sGuestProfile = null;
 
     private boolean mInGuestMode = false;
-    private static GeckoProfile mGuestProfile = null;
 
-    private static final String MOZILLA_DIR_NAME = "mozilla";
-    private static File sMozillaDir;
-
-    private static INIParser getProfilesINI(File mozillaDir) {
-        File profilesIni = new File(mozillaDir, "profiles.ini");
-        return new INIParser(profilesIni);
-    }
 
     public static GeckoProfile get(Context context) {
         boolean isGeckoApp = false;
         try {
             isGeckoApp = context instanceof GeckoApp;
         } catch (NoClassDefFoundError ex) {}
-        
 
         if (isGeckoApp) {
             // Check for a cached profile on this context already
             // TODO: We should not be caching profile information on the Activity context
-            if (((GeckoApp)context).mProfile != null) {
-                return ((GeckoApp)context).mProfile;
+            final GeckoApp geckoApp = (GeckoApp) context;
+            if (geckoApp.mProfile != null) {
+                return geckoApp.mProfile;
             }
         }
 
         // If the guest profile exists and is locked, return it
         GeckoProfile guest = GeckoProfile.getGuestProfile(context);
         if (guest != null && guest.locked()) {
             return guest;
         }
 
         if (isGeckoApp) {
-            // Otherwise, get the default profile for the Activity
-            return get(context, ((GeckoApp)context).getDefaultProfileName());
+            final GeckoApp geckoApp = (GeckoApp) context;
+            String defaultProfileName;
+            try {
+                defaultProfileName = geckoApp.getDefaultProfileName();
+            } catch (NoMozillaDirectoryException e) {
+                // If this failed, we're screwed. But there are so many callers that
+                // we'll just throw a RuntimeException.
+                Log.wtf(LOGTAG, "Unable to get default profile name.", e);
+                throw new RuntimeException(e);
+            }
+            // Otherwise, get the default profile for the Activity.
+            return get(context, defaultProfileName);
         }
 
         return get(context, "");
     }
 
     public static GeckoProfile get(Context context, String profileName) {
         synchronized (sProfileCache) {
             GeckoProfile profile = sProfileCache.get(profileName);
@@ -107,51 +115,50 @@ public final class GeckoProfile {
     public static GeckoProfile get(Context context, String profileName, File profileDir) {
         if (context == null) {
             throw new IllegalArgumentException("context must be non-null");
         }
 
         // if no profile was passed in, look for the default profile listed in profiles.ini
         // if that doesn't exist, look for a profile called 'default'
         if (TextUtils.isEmpty(profileName) && profileDir == null) {
-            profileName = GeckoProfile.findDefaultProfile(context);
-            if (profileName == null)
-                profileName = DEFAULT_PROFILE;
+            try {
+                profileName = GeckoProfile.getDefaultProfileName(context);
+            } catch (NoMozillaDirectoryException e) {
+                // We're unable to do anything sane here.
+                throw new RuntimeException(e);
+            }
         }
 
         // actually try to look up the profile
         synchronized (sProfileCache) {
             GeckoProfile profile = sProfileCache.get(profileName);
             if (profile == null) {
-                profile = new GeckoProfile(context, profileName);
+                try {
+                    profile = new GeckoProfile(context, profileName);
+                } catch (NoMozillaDirectoryException e) {
+                    // We're unable to do anything sane here.
+                    throw new RuntimeException(e);
+                }
                 profile.setDir(profileDir);
                 sProfileCache.put(profileName, profile);
             } else {
                 profile.setDir(profileDir);
             }
             return profile;
         }
     }
 
-    private static File getMozillaDirectory(Context context) {
-        return new File(context.getFilesDir(), MOZILLA_DIR_NAME);
-    }
-
-    private synchronized File ensureMozillaDirectory() throws IOException {
-        if (sMozillaDir.exists() || sMozillaDir.mkdirs()) {
-            return sMozillaDir;
+    public static boolean removeProfile(Context context, String profileName) {
+        try {
+            return new GeckoProfile(context, profileName).remove();
+        } catch (NoMozillaDirectoryException e) {
+            Log.w(LOGTAG, "Unable to remove profile: no Mozilla directory.", e);
+            return true;
         }
-
-        // Although this leaks a path to the system log, the path is
-        // predictable (unlike a profile directory), so this is fine.
-        throw new IOException("Unable to create mozilla directory at " + sMozillaDir.getAbsolutePath());
-    }
-
-    public static boolean removeProfile(Context context, String profileName) {
-        return new GeckoProfile(context, profileName).remove();
     }
 
     public static GeckoProfile createGuestProfile(Context context) {
         try {
             removeGuestProfile(context);
             // We need to force the creation of a new guest profile if we want it outside of the normal profile path,
             // otherwise GeckoProfile.getDir will try to be smart and build it for us in the normal profiles dir.
             getGuestDir(context).mkdir();
@@ -167,32 +174,32 @@ public final class GeckoProfile {
     public static void leaveGuestSession(Context context) {
         GeckoProfile profile = getGuestProfile(context);
         if (profile != null) {
             profile.unlock();
         }
     }
 
     private static File getGuestDir(Context context) {
-        if (mGuestDir == null) {
-            mGuestDir = context.getFileStreamPath("guest");
+        if (sGuestDir == null) {
+            sGuestDir = context.getFileStreamPath("guest");
         }
-        return mGuestDir;
+        return sGuestDir;
     }
 
     private static GeckoProfile getGuestProfile(Context context) {
-        if (mGuestProfile == null) {
+        if (sGuestProfile == null) {
             File guestDir = getGuestDir(context);
             if (guestDir.exists()) {
-                mGuestProfile = get(context, "guest", guestDir);
-                mGuestProfile.mInGuestMode = true;
+                sGuestProfile = get(context, "guest", guestDir);
+                sGuestProfile.mInGuestMode = true;
             }
         }
 
-        return mGuestProfile;
+        return sGuestProfile;
     }
 
     public static boolean maybeCleanupGuestProfile(final Context context) {
         final GeckoProfile profile = getGuestProfile(context);
 
         if (profile == null) {
             return false;
         } else if (!profile.locked()) {
@@ -230,16 +237,21 @@ public final class GeckoProfile {
                 delete(fileDelete);
             }
         }
 
         // Even if this is a dir, it should now be empty and delete should work
         return file.delete();
     }
 
+    private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException {
+        mName = profileName;
+        mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
+    }
+
     // Warning, Changing the lock file state from outside apis will cause this to become out of sync
     public boolean locked() {
         if (mLocked != LockState.UNDEFINED) {
             return mLocked == LockState.LOCKED;
         }
 
         // Don't use getDir() as it will create a dir if none exists
         if (mProfileDir != null && mProfileDir.exists()) {
@@ -288,23 +300,16 @@ public final class GeckoProfile {
             return result;
         } catch(IOException ex) {
             Log.e(LOGTAG, "Error unlocking profile", ex);
         }
         mLocked = LockState.LOCKED;
         return false;
     }
 
-    private GeckoProfile(Context context, String profileName) {
-        mName = profileName;
-        if (sMozillaDir == null) {
-            sMozillaDir = getMozillaDirectory(context);
-        }
-    }
-
     public boolean inGuestMode() {
         return mInGuestMode;
     }
 
     private void setDir(File dir) {
         if (dir != null && dir.exists() && dir.isDirectory()) {
             mProfileDir = dir;
         } else {
@@ -323,23 +328,22 @@ public final class GeckoProfile {
 
     public synchronized GeckoProfile forceCreate() {
         if (mProfileDir != null) {
             return this;
         }
 
         try {
             // Check if a profile with this name already exists.
-            File mozillaDir = ensureMozillaDirectory();
-            mProfileDir = findProfileDir(mozillaDir);
-            if (mProfileDir == null) {
-                // otherwise create it
-                mProfileDir = createProfileDir(mozillaDir);
-            } else {
+            try {
+                mProfileDir = findProfileDir();
                 Log.d(LOGTAG, "Found profile dir.");
+            } catch (NoSuchProfileException noSuchProfile) {
+                // If it doesn't exist, create it.
+                mProfileDir = createProfileDir();
             }
         } catch (IOException ioe) {
             Log.e(LOGTAG, "Error getting profile dir", ioe);
         }
         return this;
     }
 
     public File getFile(String aFile) {
@@ -415,49 +419,51 @@ public final class GeckoProfile {
             return sb.toString();
         } finally {
             fr.close();
         }
     }
 
     private boolean remove() {
         try {
-            File dir = getDir();
-            if (dir.exists())
+            final File dir = getDir();
+            if (dir.exists()) {
                 delete(dir);
+            }
 
-            File mozillaDir = ensureMozillaDirectory();
-            mProfileDir = findProfileDir(mozillaDir);
-            if (mProfileDir == null) {
+            try {
+                mProfileDir = findProfileDir();
+            } catch (NoSuchProfileException noSuchProfile) {
+                // If the profile doesn't exist, there's nothing left for us to do.
                 return false;
             }
 
-            INIParser parser = getProfilesINI(mozillaDir);
-
-            Hashtable<String, INISection> sections = parser.getSections();
+            final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
+            final Hashtable<String, INISection> sections = parser.getSections();
             for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
-                INISection section = e.nextElement();
+                final INISection section = e.nextElement();
                 String name = section.getStringProperty("Name");
 
-                if (name == null || !name.equals(mName))
+                if (name == null || !name.equals(mName)) {
                     continue;
+                }
 
                 if (section.getName().startsWith("Profile")) {
                     // ok, we have stupid Profile#-named things.  Rename backwards.
                     try {
                         int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
                         String curSection = "Profile" + sectionNumber;
                         String nextSection = "Profile" + (sectionNumber+1);
 
                         sections.remove(curSection);
 
                         while (sections.containsKey(nextSection)) {
                             parser.renameSection(nextSection, curSection);
                             sectionNumber++;
-                            
+
                             curSection = nextSection;
                             nextSection = "Profile" + (sectionNumber+1);
                         }
                     } catch (NumberFormatException nex) {
                         // uhm, malformed Profile thing; we can't do much.
                         Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
                         return false;
                     }
@@ -471,76 +477,56 @@ public final class GeckoProfile {
             parser.write();
             return true;
         } catch (IOException ex) {
             Log.w(LOGTAG, "Failed to remove profile.", ex);
             return false;
         }
     }
 
-    public static String findDefaultProfile(Context context) {
+    /**
+     * @return the default profile name for this application, or
+     *         {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
+     *
+     * @throws NoMozillaDirectoryException
+     *             if the Mozilla directory did not exist and could not be
+     *             created.
+     */
+    public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
         // Have we read the default profile from the INI already?
         // Changing the default profile requires a restart, so we don't
         // need to worry about runtime changes.
         if (sDefaultProfileName != null) {
             return sDefaultProfileName;
         }
 
-        // Open profiles.ini to find the correct path
-        INIParser parser = getProfilesINI(getMozillaDirectory(context));
-
-        for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
-            INISection section = e.nextElement();
-            if (section.getIntProperty("Default") == 1) {
-                sDefaultProfileName = section.getStringProperty("Name");
-                return sDefaultProfileName;
-            }
+        final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
+        if (profileName == null) {
+            // Note that we don't persist this back to profiles.ini.
+            sDefaultProfileName = DEFAULT_PROFILE;
+            return DEFAULT_PROFILE;
         }
 
-        return null;
+        sDefaultProfileName = profileName;
+        return sDefaultProfileName;
     }
 
-    private File findProfileDir(File mozillaDir) {
-        // Open profiles.ini to find the correct path
-        INIParser parser = getProfilesINI(mozillaDir);
-
-        for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
-            INISection section = e.nextElement();
-            String name = section.getStringProperty("Name");
-            if (name != null && name.equals(mName)) {
-                if (section.getIntProperty("IsRelative") == 1) {
-                    return new File(mozillaDir, section.getStringProperty("Path"));
-                }
-                return new File(section.getStringProperty("Path"));
-            }
-        }
-
-        return null;
+    private File findProfileDir() throws NoSuchProfileException {
+        return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
     }
 
-    private static String saltProfileName(String name) {
-        String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
-        StringBuilder salt = new StringBuilder(16);
-        for (int i = 0; i < 8; i++) {
-            salt.append(allowedChars.charAt((int)(Math.random() * allowedChars.length())));
-        }
-        salt.append('.');
-        salt.append(name);
-        return salt.toString();
-    }
-
-    private File createProfileDir(File mozillaDir) throws IOException {
-        INIParser parser = getProfilesINI(mozillaDir);
+    private File createProfileDir() throws IOException {
+        INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
 
         // Salt the name of our requested profile
-        String saltedName = saltProfileName(mName);
-        File profileDir = new File(mozillaDir, saltedName);
+        String saltedName = GeckoProfileDirectories.saltProfileName(mName);
+        File profileDir = new File(mMozillaDir, saltedName);
         while (profileDir.exists()) {
-            saltedName = saltProfileName(mName);
-            profileDir = new File(mozillaDir, saltedName);
+            saltedName = GeckoProfileDirectories.saltProfileName(mName);
+            profileDir = new File(mMozillaDir, saltedName);
         }
 
         // Attempt to create the salted profile dir
         if (!profileDir.mkdirs()) {
             throw new IOException("Unable to create profile.");
         }
         Log.d(LOGTAG, "Created new profile dir.");
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/GeckoProfileDirectories.java
@@ -0,0 +1,229 @@
+/* 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;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.util.INIParser;
+import org.mozilla.gecko.util.INISection;
+
+import android.content.Context;
+
+/**
+ * <code>GeckoProfileDirectories</code> manages access to mappings from profile
+ * names to salted profile directory paths, as well as the default profile name.
+ *
+ * This class will eventually come to encapsulate the remaining logic embedded
+ * in profiles.ini; for now it's a read-only wrapper.
+ */
+public class GeckoProfileDirectories {
+    @SuppressWarnings("serial")
+    public static class NoMozillaDirectoryException extends Exception {
+        public NoMozillaDirectoryException(Throwable cause) {
+            super(cause);
+        }
+
+        public NoMozillaDirectoryException(String reason) {
+            super(reason);
+        }
+
+        public NoMozillaDirectoryException(String reason, Throwable cause) {
+            super(reason, cause);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    public static class NoSuchProfileException extends Exception {
+        public NoSuchProfileException(String detailMessage, Throwable cause) {
+            super(detailMessage, cause);
+        }
+
+        public NoSuchProfileException(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    private interface INISectionPredicate {
+        public boolean matches(INISection section);
+    }
+
+    private static final String MOZILLA_DIR_NAME = "mozilla";
+
+    /**
+     * Returns true if the supplied profile entry represents the default profile.
+     */
+    private static INISectionPredicate sectionIsDefault = new INISectionPredicate() {
+        @Override
+        public boolean matches(INISection section) {
+            return section.getIntProperty("Default") == 1;
+        }
+    };
+
+    /**
+     * Returns true if the supplied profile entry has a 'Name' field.
+     */
+    private static INISectionPredicate sectionHasName = new INISectionPredicate() {
+        @Override
+        public boolean matches(INISection section) {
+            final String name = section.getStringProperty("Name");
+            return name != null;
+        }
+    };
+
+    /**
+     * Package-scoped because GeckoProfile needs to dig into this in order to do writes.
+     * This will be fixed in Bug 975212.
+     */
+    static INIParser getProfilesINI(File mozillaDir) {
+        return new INIParser(new File(mozillaDir, "profiles.ini"));
+    }
+
+    /**
+     * Utility method to compute a salted profile name: eight random alphanumeric
+     * characters, followed by a period, followed by the profile name.
+     */
+    public static String saltProfileName(final String name) {
+        if (name == null) {
+            throw new IllegalArgumentException("Cannot salt null profile name.");
+        }
+
+        final String allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+        final int scale = allowedChars.length();
+        final int saltSize = 8;
+
+        final StringBuilder saltBuilder = new StringBuilder(saltSize + 1 + name.length());
+        for (int i = 0; i < saltSize; i++) {
+            saltBuilder.append(allowedChars.charAt((int)(Math.random() * scale)));
+        }
+        saltBuilder.append('.');
+        saltBuilder.append(name);
+        return saltBuilder.toString();
+    }
+
+    /**
+     * Return the Mozilla directory within the files directory of the provided
+     * context. This should always be the same within a running application.
+     *
+     * This method is package-scoped so that new {@link GeckoProfile} instances can
+     * contextualize themselves.
+     *
+     * @return a new File object for the Mozilla directory.
+     * @throws NoMozillaDirectoryException
+     *             if the directory did not exist and could not be created.
+     */
+    static File getMozillaDirectory(Context context) throws NoMozillaDirectoryException {
+        final File mozillaDir = new File(context.getFilesDir(), MOZILLA_DIR_NAME);
+        if (mozillaDir.exists() || mozillaDir.mkdirs()) {
+            return mozillaDir;
+        }
+
+        // Although this leaks a path to the system log, the path is
+        // predictable (unlike a profile directory), so this is fine.
+        throw new NoMozillaDirectoryException("Unable to create mozilla directory at " + mozillaDir.getAbsolutePath());
+    }
+
+    /**
+     * Discover the default profile name by examining profiles.ini.
+     *
+     * Package-scoped because {@link GeckoProfile} needs access to it.
+     *
+     * @return null if there is no "Default" entry in profiles.ini, or the profile
+     *         name if there is.
+     * @throws NoMozillaDirectoryException
+     *             if the Mozilla directory did not exist and could not be created.
+     */
+    static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
+      final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
+
+      for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
+          final INISection section = e.nextElement();
+          if (section.getIntProperty("Default") == 1) {
+              return section.getStringProperty("Name");
+          }
+      }
+
+      return null;
+    }
+
+    static Map<String, String> getDefaultProfile(final File mozillaDir) {
+        return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
+    }
+
+    static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
+        final INISectionPredicate predicate = new INISectionPredicate() {
+            @Override
+            public boolean matches(final INISection section) {
+                return name.equals(section.getStringProperty("Name"));
+            }
+        };
+        return getMatchingProfiles(mozillaDir, predicate, true);
+    }
+
+    /**
+     * Calls {@link GeckoProfileDirectories#getMatchingProfiles(File, INISectionPredicate, boolean)}
+     * with a filter to ensure that all profiles are named.
+     */
+    static Map<String, String> getAllProfiles(final File mozillaDir) {
+        return getMatchingProfiles(mozillaDir, sectionHasName, false);
+    }
+
+    /**
+     * Return a mapping from the names of all matching profiles (that is,
+     * profiles appearing in profiles.ini that match the supplied predicate) to
+     * their absolute paths on disk.
+     *
+     * @param mozillaDir
+     *            a directory containing profiles.ini.
+     * @param predicate
+     *            a predicate to use when evaluating whether to include a
+     *            particular INI section.
+     * @param stopOnSuccess
+     *            if true, this method will return with the first result that
+     *            matches the predicate; if false, all matching results are
+     *            included.
+     * @return a {@link Map} from name to path.
+     */
+    public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
+        final HashMap<String, String> result = new HashMap<String, String>();
+        final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+
+        for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
+            final INISection section = e.nextElement();
+            if (predicate == null || predicate.matches(section)) {
+                final String name = section.getStringProperty("Name");
+                final String pathString = section.getStringProperty("Path");
+                final boolean isRelative = section.getIntProperty("IsRelative") == 1;
+                final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
+                result.put(name, path.getAbsolutePath());
+
+                if (stopOnSuccess) {
+                    return result;
+                }
+            }
+        }
+        return result;
+    }
+
+    public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
+        // Open profiles.ini to find the correct path.
+        final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
+
+        for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
+            final INISection section = e.nextElement();
+            final String name = section.getStringProperty("Name");
+            if (name != null && name.equals(profileName)) {
+                if (section.getIntProperty("IsRelative") == 1) {
+                    return new File(mozillaDir, section.getStringProperty("Path"));
+                }
+                return new File(section.getStringProperty("Path"));
+            }
+        }
+
+        throw new NoSuchProfileException("No profile " + profileName);
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -149,16 +149,17 @@ gbjar.sources += [
     'GeckoEditable.java',
     'GeckoEvent.java',
     'GeckoHalDefines.java',
     'GeckoInputConnection.java',
     'GeckoJavaSampler.java',
     'GeckoMessageReceiver.java',
     'GeckoNetworkManager.java',
     'GeckoProfile.java',
+    'GeckoProfileDirectories.java',
     'GeckoScreenOrientation.java',
     'GeckoSmsManager.java',
     'GeckoThread.java',
     'GeckoUpdateReceiver.java',
     'GeckoView.java',
     'GeckoViewChrome.java',
     'GeckoViewContent.java',
     'gfx/Axis.java',