michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; michael@0: import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException; michael@0: import org.mozilla.gecko.Telemetry; michael@0: import org.mozilla.gecko.TelemetryContract; michael@0: import org.mozilla.gecko.util.INIParser; michael@0: import org.mozilla.gecko.util.INISection; michael@0: michael@0: import android.content.Context; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: michael@0: import java.io.File; michael@0: import java.io.FileOutputStream; michael@0: import java.io.FileReader; michael@0: import java.io.IOException; michael@0: import java.io.OutputStreamWriter; michael@0: import java.nio.charset.Charset; michael@0: import java.util.Enumeration; michael@0: import java.util.HashMap; michael@0: import java.util.Hashtable; michael@0: michael@0: public final class GeckoProfile { michael@0: private static final String LOGTAG = "GeckoProfile"; michael@0: michael@0: // Used to "lock" the guest profile, so that we'll always restart in it michael@0: private static final String LOCK_FILE_NAME = ".active_lock"; michael@0: public static final String DEFAULT_PROFILE = "default"; michael@0: private static final String GUEST_PROFILE = "guest"; michael@0: michael@0: private static HashMap sProfileCache = new HashMap(); michael@0: private static String sDefaultProfileName = null; michael@0: michael@0: public static boolean sIsUsingCustomProfile = false; michael@0: private final String mName; michael@0: private final File mMozillaDir; michael@0: private File mProfileDir; // Not final because this is lazily computed. michael@0: michael@0: // Constants to cache whether or not a profile is "locked". michael@0: private enum LockState { michael@0: LOCKED, michael@0: UNLOCKED, michael@0: UNDEFINED michael@0: }; michael@0: michael@0: // Caches whether or not a profile is "locked". Only used by the guest profile to determine if it should michael@0: // be reused or deleted on startup michael@0: private LockState mLocked = LockState.UNDEFINED; michael@0: michael@0: // Caches the guest profile dir. michael@0: private static File sGuestDir = null; michael@0: private static GeckoProfile sGuestProfile = null; michael@0: michael@0: private boolean mInGuestMode = false; michael@0: michael@0: michael@0: public static GeckoProfile get(Context context) { michael@0: boolean isGeckoApp = false; michael@0: try { michael@0: isGeckoApp = context instanceof GeckoApp; michael@0: } catch (NoClassDefFoundError ex) {} michael@0: michael@0: if (isGeckoApp) { michael@0: // Check for a cached profile on this context already michael@0: // TODO: We should not be caching profile information on the Activity context michael@0: final GeckoApp geckoApp = (GeckoApp) context; michael@0: if (geckoApp.mProfile != null) { michael@0: return geckoApp.mProfile; michael@0: } michael@0: } michael@0: michael@0: // If the guest profile exists and is locked, return it michael@0: GeckoProfile guest = GeckoProfile.getGuestProfile(context); michael@0: if (guest != null && guest.locked()) { michael@0: return guest; michael@0: } michael@0: michael@0: if (isGeckoApp) { michael@0: final GeckoApp geckoApp = (GeckoApp) context; michael@0: String defaultProfileName; michael@0: try { michael@0: defaultProfileName = geckoApp.getDefaultProfileName(); michael@0: } catch (NoMozillaDirectoryException e) { michael@0: // If this failed, we're screwed. But there are so many callers that michael@0: // we'll just throw a RuntimeException. michael@0: Log.wtf(LOGTAG, "Unable to get default profile name.", e); michael@0: throw new RuntimeException(e); michael@0: } michael@0: // Otherwise, get the default profile for the Activity. michael@0: return get(context, defaultProfileName); michael@0: } michael@0: michael@0: return get(context, ""); michael@0: } michael@0: michael@0: public static GeckoProfile get(Context context, String profileName) { michael@0: synchronized (sProfileCache) { michael@0: GeckoProfile profile = sProfileCache.get(profileName); michael@0: if (profile != null) michael@0: return profile; michael@0: } michael@0: return get(context, profileName, (File)null); michael@0: } michael@0: michael@0: public static GeckoProfile get(Context context, String profileName, String profilePath) { michael@0: File dir = null; michael@0: if (!TextUtils.isEmpty(profilePath)) { michael@0: dir = new File(profilePath); michael@0: if (!dir.exists() || !dir.isDirectory()) { michael@0: Log.w(LOGTAG, "requested profile directory missing: " + profilePath); michael@0: } michael@0: } michael@0: return get(context, profileName, dir); michael@0: } michael@0: michael@0: public static GeckoProfile get(Context context, String profileName, File profileDir) { michael@0: if (context == null) { michael@0: throw new IllegalArgumentException("context must be non-null"); michael@0: } michael@0: michael@0: // if no profile was passed in, look for the default profile listed in profiles.ini michael@0: // if that doesn't exist, look for a profile called 'default' michael@0: if (TextUtils.isEmpty(profileName) && profileDir == null) { michael@0: try { michael@0: profileName = GeckoProfile.getDefaultProfileName(context); michael@0: } catch (NoMozillaDirectoryException e) { michael@0: // We're unable to do anything sane here. michael@0: throw new RuntimeException(e); michael@0: } michael@0: } michael@0: michael@0: // actually try to look up the profile michael@0: synchronized (sProfileCache) { michael@0: GeckoProfile profile = sProfileCache.get(profileName); michael@0: if (profile == null) { michael@0: try { michael@0: profile = new GeckoProfile(context, profileName); michael@0: } catch (NoMozillaDirectoryException e) { michael@0: // We're unable to do anything sane here. michael@0: throw new RuntimeException(e); michael@0: } michael@0: profile.setDir(profileDir); michael@0: sProfileCache.put(profileName, profile); michael@0: } else { michael@0: profile.setDir(profileDir); michael@0: } michael@0: return profile; michael@0: } michael@0: } michael@0: michael@0: public static boolean removeProfile(Context context, String profileName) { michael@0: final boolean success; michael@0: try { michael@0: success = new GeckoProfile(context, profileName).remove(); michael@0: } catch (NoMozillaDirectoryException e) { michael@0: Log.w(LOGTAG, "Unable to remove profile: no Mozilla directory.", e); michael@0: return true; michael@0: } michael@0: michael@0: if (success) { michael@0: // Clear all shared prefs for the given profile. michael@0: GeckoSharedPrefs.forProfileName(context, profileName) michael@0: .edit().clear().commit(); michael@0: } michael@0: michael@0: return success; michael@0: } michael@0: michael@0: public static GeckoProfile createGuestProfile(Context context) { michael@0: try { michael@0: removeGuestProfile(context); michael@0: // We need to force the creation of a new guest profile if we want it outside of the normal profile path, michael@0: // otherwise GeckoProfile.getDir will try to be smart and build it for us in the normal profiles dir. michael@0: getGuestDir(context).mkdir(); michael@0: GeckoProfile profile = getGuestProfile(context); michael@0: profile.lock(); michael@0: return profile; michael@0: } catch (Exception ex) { michael@0: Log.e(LOGTAG, "Error creating guest profile", ex); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: public static void leaveGuestSession(Context context) { michael@0: GeckoProfile profile = getGuestProfile(context); michael@0: if (profile != null) { michael@0: profile.unlock(); michael@0: } michael@0: } michael@0: michael@0: private static File getGuestDir(Context context) { michael@0: if (sGuestDir == null) { michael@0: sGuestDir = context.getFileStreamPath("guest"); michael@0: } michael@0: return sGuestDir; michael@0: } michael@0: michael@0: private static GeckoProfile getGuestProfile(Context context) { michael@0: if (sGuestProfile == null) { michael@0: File guestDir = getGuestDir(context); michael@0: if (guestDir.exists()) { michael@0: sGuestProfile = get(context, GUEST_PROFILE, guestDir); michael@0: sGuestProfile.mInGuestMode = true; michael@0: } michael@0: } michael@0: michael@0: return sGuestProfile; michael@0: } michael@0: michael@0: public static boolean maybeCleanupGuestProfile(final Context context) { michael@0: final GeckoProfile profile = getGuestProfile(context); michael@0: michael@0: if (profile == null) { michael@0: return false; michael@0: } else if (!profile.locked()) { michael@0: profile.mInGuestMode = false; michael@0: michael@0: // If the guest dir exists, but it's unlocked, delete it michael@0: removeGuestProfile(context); michael@0: michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: private static void removeGuestProfile(Context context) { michael@0: boolean success = false; michael@0: try { michael@0: File guestDir = getGuestDir(context); michael@0: if (guestDir.exists()) { michael@0: success = delete(guestDir); michael@0: } michael@0: } catch (Exception ex) { michael@0: Log.e(LOGTAG, "Error removing guest profile", ex); michael@0: } michael@0: michael@0: if (success) { michael@0: // Clear all shared prefs for the guest profile. michael@0: GeckoSharedPrefs.forProfileName(context, GUEST_PROFILE) michael@0: .edit().clear().commit(); michael@0: } michael@0: } michael@0: michael@0: public static boolean delete(File file) throws IOException { michael@0: // Try to do a quick initial delete michael@0: if (file.delete()) michael@0: return true; michael@0: michael@0: if (file.isDirectory()) { michael@0: // If the quick delete failed and this is a dir, recursively delete the contents of the dir michael@0: String files[] = file.list(); michael@0: for (String temp : files) { michael@0: File fileDelete = new File(file, temp); michael@0: delete(fileDelete); michael@0: } michael@0: } michael@0: michael@0: // Even if this is a dir, it should now be empty and delete should work michael@0: return file.delete(); michael@0: } michael@0: michael@0: private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException { michael@0: mName = profileName; michael@0: mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context); michael@0: } michael@0: michael@0: // Warning, Changing the lock file state from outside apis will cause this to become out of sync michael@0: public boolean locked() { michael@0: if (mLocked != LockState.UNDEFINED) { michael@0: return mLocked == LockState.LOCKED; michael@0: } michael@0: michael@0: // Don't use getDir() as it will create a dir if none exists michael@0: if (mProfileDir != null && mProfileDir.exists()) { michael@0: File lockFile = new File(mProfileDir, LOCK_FILE_NAME); michael@0: boolean res = lockFile.exists(); michael@0: mLocked = res ? LockState.LOCKED : LockState.UNLOCKED; michael@0: } else { michael@0: mLocked = LockState.UNLOCKED; michael@0: } michael@0: michael@0: return mLocked == LockState.LOCKED; michael@0: } michael@0: michael@0: public boolean lock() { michael@0: try { michael@0: // If this dir doesn't exist getDir will create it for us michael@0: File lockFile = new File(getDir(), LOCK_FILE_NAME); michael@0: boolean result = lockFile.createNewFile(); michael@0: if (result) { michael@0: mLocked = LockState.LOCKED; michael@0: } else { michael@0: mLocked = LockState.UNLOCKED; michael@0: } michael@0: return result; michael@0: } catch(IOException ex) { michael@0: Log.e(LOGTAG, "Error locking profile", ex); michael@0: } michael@0: mLocked = LockState.UNLOCKED; michael@0: return false; michael@0: } michael@0: michael@0: public boolean unlock() { michael@0: // Don't use getDir() as it will create a dir michael@0: if (mProfileDir == null || !mProfileDir.exists()) { michael@0: return true; michael@0: } michael@0: michael@0: try { michael@0: File lockFile = new File(mProfileDir, LOCK_FILE_NAME); michael@0: boolean result = delete(lockFile); michael@0: if (result) { michael@0: mLocked = LockState.UNLOCKED; michael@0: } else { michael@0: mLocked = LockState.LOCKED; michael@0: } michael@0: return result; michael@0: } catch(IOException ex) { michael@0: Log.e(LOGTAG, "Error unlocking profile", ex); michael@0: } michael@0: mLocked = LockState.LOCKED; michael@0: return false; michael@0: } michael@0: michael@0: public boolean inGuestMode() { michael@0: return mInGuestMode; michael@0: } michael@0: michael@0: private void setDir(File dir) { michael@0: if (dir != null && dir.exists() && dir.isDirectory()) { michael@0: mProfileDir = dir; michael@0: } michael@0: } michael@0: michael@0: public String getName() { michael@0: return mName; michael@0: } michael@0: michael@0: public synchronized File getDir() { michael@0: forceCreate(); michael@0: return mProfileDir; michael@0: } michael@0: michael@0: public synchronized GeckoProfile forceCreate() { michael@0: if (mProfileDir != null) { michael@0: return this; michael@0: } michael@0: michael@0: try { michael@0: // Check if a profile with this name already exists. michael@0: try { michael@0: mProfileDir = findProfileDir(); michael@0: Log.d(LOGTAG, "Found profile dir."); michael@0: } catch (NoSuchProfileException noSuchProfile) { michael@0: // If it doesn't exist, create it. michael@0: mProfileDir = createProfileDir(); michael@0: } michael@0: } catch (IOException ioe) { michael@0: Log.e(LOGTAG, "Error getting profile dir", ioe); michael@0: } michael@0: return this; michael@0: } michael@0: michael@0: public File getFile(String aFile) { michael@0: File f = getDir(); michael@0: if (f == null) michael@0: return null; michael@0: michael@0: return new File(f, aFile); michael@0: } michael@0: michael@0: /** michael@0: * Moves the session file to the backup session file. michael@0: * michael@0: * sessionstore.js should hold the current session, and sessionstore.bak michael@0: * should hold the previous session (where it is used to read the "tabs michael@0: * from last time"). Normally, sessionstore.js is moved to sessionstore.bak michael@0: * on a clean quit, but this doesn't happen if Fennec crashed. Thus, this michael@0: * method should be called after a crash so sessionstore.bak correctly michael@0: * holds the previous session. michael@0: */ michael@0: public void moveSessionFile() { michael@0: File sessionFile = getFile("sessionstore.js"); michael@0: if (sessionFile != null && sessionFile.exists()) { michael@0: File sessionFileBackup = getFile("sessionstore.bak"); michael@0: sessionFile.renameTo(sessionFileBackup); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Get the string from a session file. michael@0: * michael@0: * The session can either be read from sessionstore.js or sessionstore.bak. michael@0: * In general, sessionstore.js holds the current session, and michael@0: * sessionstore.bak holds the previous session. michael@0: * michael@0: * @param readBackup if true, the session is read from sessionstore.bak; michael@0: * otherwise, the session is read from sessionstore.js michael@0: * michael@0: * @return the session string michael@0: */ michael@0: public String readSessionFile(boolean readBackup) { michael@0: File sessionFile = getFile(readBackup ? "sessionstore.bak" : "sessionstore.js"); michael@0: michael@0: try { michael@0: if (sessionFile != null && sessionFile.exists()) { michael@0: return readFile(sessionFile); michael@0: } michael@0: } catch (IOException ioe) { michael@0: Log.e(LOGTAG, "Unable to read session file", ioe); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: public String readFile(String filename) throws IOException { michael@0: File dir = getDir(); michael@0: if (dir == null) { michael@0: throw new IOException("No profile directory found"); michael@0: } michael@0: File target = new File(dir, filename); michael@0: return readFile(target); michael@0: } michael@0: michael@0: private String readFile(File target) throws IOException { michael@0: FileReader fr = new FileReader(target); michael@0: try { michael@0: StringBuilder sb = new StringBuilder(); michael@0: char[] buf = new char[8192]; michael@0: int read = fr.read(buf); michael@0: while (read >= 0) { michael@0: sb.append(buf, 0, read); michael@0: read = fr.read(buf); michael@0: } michael@0: return sb.toString(); michael@0: } finally { michael@0: fr.close(); michael@0: } michael@0: } michael@0: michael@0: private boolean remove() { michael@0: try { michael@0: final File dir = getDir(); michael@0: if (dir.exists()) { michael@0: delete(dir); michael@0: } michael@0: michael@0: try { michael@0: mProfileDir = findProfileDir(); michael@0: } catch (NoSuchProfileException noSuchProfile) { michael@0: // If the profile doesn't exist, there's nothing left for us to do. michael@0: return false; michael@0: } michael@0: michael@0: final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir); michael@0: final Hashtable sections = parser.getSections(); michael@0: for (Enumeration e = sections.elements(); e.hasMoreElements();) { michael@0: final INISection section = e.nextElement(); michael@0: String name = section.getStringProperty("Name"); michael@0: michael@0: if (name == null || !name.equals(mName)) { michael@0: continue; michael@0: } michael@0: michael@0: if (section.getName().startsWith("Profile")) { michael@0: // ok, we have stupid Profile#-named things. Rename backwards. michael@0: try { michael@0: int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length())); michael@0: String curSection = "Profile" + sectionNumber; michael@0: String nextSection = "Profile" + (sectionNumber+1); michael@0: michael@0: sections.remove(curSection); michael@0: michael@0: while (sections.containsKey(nextSection)) { michael@0: parser.renameSection(nextSection, curSection); michael@0: sectionNumber++; michael@0: michael@0: curSection = nextSection; michael@0: nextSection = "Profile" + (sectionNumber+1); michael@0: } michael@0: } catch (NumberFormatException nex) { michael@0: // uhm, malformed Profile thing; we can't do much. michael@0: Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName()); michael@0: return false; michael@0: } michael@0: } else { michael@0: // this really shouldn't be the case, but handle it anyway michael@0: parser.removeSection(mName); michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: parser.write(); michael@0: return true; michael@0: } catch (IOException ex) { michael@0: Log.w(LOGTAG, "Failed to remove profile.", ex); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return the default profile name for this application, or michael@0: * {@link GeckoProfile#DEFAULT_PROFILE} if none could be found. michael@0: * michael@0: * @throws NoMozillaDirectoryException michael@0: * if the Mozilla directory did not exist and could not be michael@0: * created. michael@0: */ michael@0: public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException { michael@0: // Have we read the default profile from the INI already? michael@0: // Changing the default profile requires a restart, so we don't michael@0: // need to worry about runtime changes. michael@0: if (sDefaultProfileName != null) { michael@0: return sDefaultProfileName; michael@0: } michael@0: michael@0: final String profileName = GeckoProfileDirectories.findDefaultProfileName(context); michael@0: if (profileName == null) { michael@0: // Note that we don't persist this back to profiles.ini. michael@0: sDefaultProfileName = DEFAULT_PROFILE; michael@0: return DEFAULT_PROFILE; michael@0: } michael@0: michael@0: sDefaultProfileName = profileName; michael@0: return sDefaultProfileName; michael@0: } michael@0: michael@0: private File findProfileDir() throws NoSuchProfileException { michael@0: return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName); michael@0: } michael@0: michael@0: private File createProfileDir() throws IOException { michael@0: INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir); michael@0: michael@0: // Salt the name of our requested profile michael@0: String saltedName = GeckoProfileDirectories.saltProfileName(mName); michael@0: File profileDir = new File(mMozillaDir, saltedName); michael@0: while (profileDir.exists()) { michael@0: saltedName = GeckoProfileDirectories.saltProfileName(mName); michael@0: profileDir = new File(mMozillaDir, saltedName); michael@0: } michael@0: michael@0: // Attempt to create the salted profile dir michael@0: if (!profileDir.mkdirs()) { michael@0: throw new IOException("Unable to create profile."); michael@0: } michael@0: Log.d(LOGTAG, "Created new profile dir."); michael@0: michael@0: // Now update profiles.ini michael@0: // If this is the first time its created, we also add a General section michael@0: // look for the first profile number that isn't taken yet michael@0: int profileNum = 0; michael@0: boolean isDefaultSet = false; michael@0: INISection profileSection; michael@0: while ((profileSection = parser.getSection("Profile" + profileNum)) != null) { michael@0: profileNum++; michael@0: if (profileSection.getProperty("Default") != null) { michael@0: isDefaultSet = true; michael@0: } michael@0: } michael@0: michael@0: profileSection = new INISection("Profile" + profileNum); michael@0: profileSection.setProperty("Name", mName); michael@0: profileSection.setProperty("IsRelative", 1); michael@0: profileSection.setProperty("Path", saltedName); michael@0: michael@0: if (parser.getSection("General") == null) { michael@0: INISection generalSection = new INISection("General"); michael@0: generalSection.setProperty("StartWithLastProfile", 1); michael@0: parser.addSection(generalSection); michael@0: } michael@0: michael@0: if (!isDefaultSet && !mName.startsWith("webapp")) { michael@0: // only set as default if this is the first non-webapp michael@0: // profile we're creating michael@0: profileSection.setProperty("Default", 1); michael@0: michael@0: // We have no intention of stopping this session. The FIRSTRUN session michael@0: // ends when the browsing session/activity has ended. All events michael@0: // during firstrun will be tagged as FIRSTRUN. michael@0: Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN); michael@0: } michael@0: michael@0: parser.addSection(profileSection); michael@0: parser.write(); michael@0: michael@0: // Write out profile creation time, mirroring the logic in nsToolkitProfileService. michael@0: try { michael@0: FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + "times.json"); michael@0: OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8")); michael@0: try { michael@0: writer.append("{\"created\": " + System.currentTimeMillis() + "}\n"); michael@0: } finally { michael@0: writer.close(); michael@0: } michael@0: } catch (Exception e) { michael@0: // Best-effort. michael@0: Log.w(LOGTAG, "Couldn't write times.json.", e); michael@0: } michael@0: michael@0: return profileDir; michael@0: } michael@0: }