1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/GeckoProfile.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,609 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; 1.12 +import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException; 1.13 +import org.mozilla.gecko.Telemetry; 1.14 +import org.mozilla.gecko.TelemetryContract; 1.15 +import org.mozilla.gecko.util.INIParser; 1.16 +import org.mozilla.gecko.util.INISection; 1.17 + 1.18 +import android.content.Context; 1.19 +import android.text.TextUtils; 1.20 +import android.util.Log; 1.21 + 1.22 +import java.io.File; 1.23 +import java.io.FileOutputStream; 1.24 +import java.io.FileReader; 1.25 +import java.io.IOException; 1.26 +import java.io.OutputStreamWriter; 1.27 +import java.nio.charset.Charset; 1.28 +import java.util.Enumeration; 1.29 +import java.util.HashMap; 1.30 +import java.util.Hashtable; 1.31 + 1.32 +public final class GeckoProfile { 1.33 + private static final String LOGTAG = "GeckoProfile"; 1.34 + 1.35 + // Used to "lock" the guest profile, so that we'll always restart in it 1.36 + private static final String LOCK_FILE_NAME = ".active_lock"; 1.37 + public static final String DEFAULT_PROFILE = "default"; 1.38 + private static final String GUEST_PROFILE = "guest"; 1.39 + 1.40 + private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>(); 1.41 + private static String sDefaultProfileName = null; 1.42 + 1.43 + public static boolean sIsUsingCustomProfile = false; 1.44 + private final String mName; 1.45 + private final File mMozillaDir; 1.46 + private File mProfileDir; // Not final because this is lazily computed. 1.47 + 1.48 + // Constants to cache whether or not a profile is "locked". 1.49 + private enum LockState { 1.50 + LOCKED, 1.51 + UNLOCKED, 1.52 + UNDEFINED 1.53 + }; 1.54 + 1.55 + // Caches whether or not a profile is "locked". Only used by the guest profile to determine if it should 1.56 + // be reused or deleted on startup 1.57 + private LockState mLocked = LockState.UNDEFINED; 1.58 + 1.59 + // Caches the guest profile dir. 1.60 + private static File sGuestDir = null; 1.61 + private static GeckoProfile sGuestProfile = null; 1.62 + 1.63 + private boolean mInGuestMode = false; 1.64 + 1.65 + 1.66 + public static GeckoProfile get(Context context) { 1.67 + boolean isGeckoApp = false; 1.68 + try { 1.69 + isGeckoApp = context instanceof GeckoApp; 1.70 + } catch (NoClassDefFoundError ex) {} 1.71 + 1.72 + if (isGeckoApp) { 1.73 + // Check for a cached profile on this context already 1.74 + // TODO: We should not be caching profile information on the Activity context 1.75 + final GeckoApp geckoApp = (GeckoApp) context; 1.76 + if (geckoApp.mProfile != null) { 1.77 + return geckoApp.mProfile; 1.78 + } 1.79 + } 1.80 + 1.81 + // If the guest profile exists and is locked, return it 1.82 + GeckoProfile guest = GeckoProfile.getGuestProfile(context); 1.83 + if (guest != null && guest.locked()) { 1.84 + return guest; 1.85 + } 1.86 + 1.87 + if (isGeckoApp) { 1.88 + final GeckoApp geckoApp = (GeckoApp) context; 1.89 + String defaultProfileName; 1.90 + try { 1.91 + defaultProfileName = geckoApp.getDefaultProfileName(); 1.92 + } catch (NoMozillaDirectoryException e) { 1.93 + // If this failed, we're screwed. But there are so many callers that 1.94 + // we'll just throw a RuntimeException. 1.95 + Log.wtf(LOGTAG, "Unable to get default profile name.", e); 1.96 + throw new RuntimeException(e); 1.97 + } 1.98 + // Otherwise, get the default profile for the Activity. 1.99 + return get(context, defaultProfileName); 1.100 + } 1.101 + 1.102 + return get(context, ""); 1.103 + } 1.104 + 1.105 + public static GeckoProfile get(Context context, String profileName) { 1.106 + synchronized (sProfileCache) { 1.107 + GeckoProfile profile = sProfileCache.get(profileName); 1.108 + if (profile != null) 1.109 + return profile; 1.110 + } 1.111 + return get(context, profileName, (File)null); 1.112 + } 1.113 + 1.114 + public static GeckoProfile get(Context context, String profileName, String profilePath) { 1.115 + File dir = null; 1.116 + if (!TextUtils.isEmpty(profilePath)) { 1.117 + dir = new File(profilePath); 1.118 + if (!dir.exists() || !dir.isDirectory()) { 1.119 + Log.w(LOGTAG, "requested profile directory missing: " + profilePath); 1.120 + } 1.121 + } 1.122 + return get(context, profileName, dir); 1.123 + } 1.124 + 1.125 + public static GeckoProfile get(Context context, String profileName, File profileDir) { 1.126 + if (context == null) { 1.127 + throw new IllegalArgumentException("context must be non-null"); 1.128 + } 1.129 + 1.130 + // if no profile was passed in, look for the default profile listed in profiles.ini 1.131 + // if that doesn't exist, look for a profile called 'default' 1.132 + if (TextUtils.isEmpty(profileName) && profileDir == null) { 1.133 + try { 1.134 + profileName = GeckoProfile.getDefaultProfileName(context); 1.135 + } catch (NoMozillaDirectoryException e) { 1.136 + // We're unable to do anything sane here. 1.137 + throw new RuntimeException(e); 1.138 + } 1.139 + } 1.140 + 1.141 + // actually try to look up the profile 1.142 + synchronized (sProfileCache) { 1.143 + GeckoProfile profile = sProfileCache.get(profileName); 1.144 + if (profile == null) { 1.145 + try { 1.146 + profile = new GeckoProfile(context, profileName); 1.147 + } catch (NoMozillaDirectoryException e) { 1.148 + // We're unable to do anything sane here. 1.149 + throw new RuntimeException(e); 1.150 + } 1.151 + profile.setDir(profileDir); 1.152 + sProfileCache.put(profileName, profile); 1.153 + } else { 1.154 + profile.setDir(profileDir); 1.155 + } 1.156 + return profile; 1.157 + } 1.158 + } 1.159 + 1.160 + public static boolean removeProfile(Context context, String profileName) { 1.161 + final boolean success; 1.162 + try { 1.163 + success = new GeckoProfile(context, profileName).remove(); 1.164 + } catch (NoMozillaDirectoryException e) { 1.165 + Log.w(LOGTAG, "Unable to remove profile: no Mozilla directory.", e); 1.166 + return true; 1.167 + } 1.168 + 1.169 + if (success) { 1.170 + // Clear all shared prefs for the given profile. 1.171 + GeckoSharedPrefs.forProfileName(context, profileName) 1.172 + .edit().clear().commit(); 1.173 + } 1.174 + 1.175 + return success; 1.176 + } 1.177 + 1.178 + public static GeckoProfile createGuestProfile(Context context) { 1.179 + try { 1.180 + removeGuestProfile(context); 1.181 + // We need to force the creation of a new guest profile if we want it outside of the normal profile path, 1.182 + // otherwise GeckoProfile.getDir will try to be smart and build it for us in the normal profiles dir. 1.183 + getGuestDir(context).mkdir(); 1.184 + GeckoProfile profile = getGuestProfile(context); 1.185 + profile.lock(); 1.186 + return profile; 1.187 + } catch (Exception ex) { 1.188 + Log.e(LOGTAG, "Error creating guest profile", ex); 1.189 + } 1.190 + return null; 1.191 + } 1.192 + 1.193 + public static void leaveGuestSession(Context context) { 1.194 + GeckoProfile profile = getGuestProfile(context); 1.195 + if (profile != null) { 1.196 + profile.unlock(); 1.197 + } 1.198 + } 1.199 + 1.200 + private static File getGuestDir(Context context) { 1.201 + if (sGuestDir == null) { 1.202 + sGuestDir = context.getFileStreamPath("guest"); 1.203 + } 1.204 + return sGuestDir; 1.205 + } 1.206 + 1.207 + private static GeckoProfile getGuestProfile(Context context) { 1.208 + if (sGuestProfile == null) { 1.209 + File guestDir = getGuestDir(context); 1.210 + if (guestDir.exists()) { 1.211 + sGuestProfile = get(context, GUEST_PROFILE, guestDir); 1.212 + sGuestProfile.mInGuestMode = true; 1.213 + } 1.214 + } 1.215 + 1.216 + return sGuestProfile; 1.217 + } 1.218 + 1.219 + public static boolean maybeCleanupGuestProfile(final Context context) { 1.220 + final GeckoProfile profile = getGuestProfile(context); 1.221 + 1.222 + if (profile == null) { 1.223 + return false; 1.224 + } else if (!profile.locked()) { 1.225 + profile.mInGuestMode = false; 1.226 + 1.227 + // If the guest dir exists, but it's unlocked, delete it 1.228 + removeGuestProfile(context); 1.229 + 1.230 + return true; 1.231 + } 1.232 + return false; 1.233 + } 1.234 + 1.235 + private static void removeGuestProfile(Context context) { 1.236 + boolean success = false; 1.237 + try { 1.238 + File guestDir = getGuestDir(context); 1.239 + if (guestDir.exists()) { 1.240 + success = delete(guestDir); 1.241 + } 1.242 + } catch (Exception ex) { 1.243 + Log.e(LOGTAG, "Error removing guest profile", ex); 1.244 + } 1.245 + 1.246 + if (success) { 1.247 + // Clear all shared prefs for the guest profile. 1.248 + GeckoSharedPrefs.forProfileName(context, GUEST_PROFILE) 1.249 + .edit().clear().commit(); 1.250 + } 1.251 + } 1.252 + 1.253 + public static boolean delete(File file) throws IOException { 1.254 + // Try to do a quick initial delete 1.255 + if (file.delete()) 1.256 + return true; 1.257 + 1.258 + if (file.isDirectory()) { 1.259 + // If the quick delete failed and this is a dir, recursively delete the contents of the dir 1.260 + String files[] = file.list(); 1.261 + for (String temp : files) { 1.262 + File fileDelete = new File(file, temp); 1.263 + delete(fileDelete); 1.264 + } 1.265 + } 1.266 + 1.267 + // Even if this is a dir, it should now be empty and delete should work 1.268 + return file.delete(); 1.269 + } 1.270 + 1.271 + private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException { 1.272 + mName = profileName; 1.273 + mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context); 1.274 + } 1.275 + 1.276 + // Warning, Changing the lock file state from outside apis will cause this to become out of sync 1.277 + public boolean locked() { 1.278 + if (mLocked != LockState.UNDEFINED) { 1.279 + return mLocked == LockState.LOCKED; 1.280 + } 1.281 + 1.282 + // Don't use getDir() as it will create a dir if none exists 1.283 + if (mProfileDir != null && mProfileDir.exists()) { 1.284 + File lockFile = new File(mProfileDir, LOCK_FILE_NAME); 1.285 + boolean res = lockFile.exists(); 1.286 + mLocked = res ? LockState.LOCKED : LockState.UNLOCKED; 1.287 + } else { 1.288 + mLocked = LockState.UNLOCKED; 1.289 + } 1.290 + 1.291 + return mLocked == LockState.LOCKED; 1.292 + } 1.293 + 1.294 + public boolean lock() { 1.295 + try { 1.296 + // If this dir doesn't exist getDir will create it for us 1.297 + File lockFile = new File(getDir(), LOCK_FILE_NAME); 1.298 + boolean result = lockFile.createNewFile(); 1.299 + if (result) { 1.300 + mLocked = LockState.LOCKED; 1.301 + } else { 1.302 + mLocked = LockState.UNLOCKED; 1.303 + } 1.304 + return result; 1.305 + } catch(IOException ex) { 1.306 + Log.e(LOGTAG, "Error locking profile", ex); 1.307 + } 1.308 + mLocked = LockState.UNLOCKED; 1.309 + return false; 1.310 + } 1.311 + 1.312 + public boolean unlock() { 1.313 + // Don't use getDir() as it will create a dir 1.314 + if (mProfileDir == null || !mProfileDir.exists()) { 1.315 + return true; 1.316 + } 1.317 + 1.318 + try { 1.319 + File lockFile = new File(mProfileDir, LOCK_FILE_NAME); 1.320 + boolean result = delete(lockFile); 1.321 + if (result) { 1.322 + mLocked = LockState.UNLOCKED; 1.323 + } else { 1.324 + mLocked = LockState.LOCKED; 1.325 + } 1.326 + return result; 1.327 + } catch(IOException ex) { 1.328 + Log.e(LOGTAG, "Error unlocking profile", ex); 1.329 + } 1.330 + mLocked = LockState.LOCKED; 1.331 + return false; 1.332 + } 1.333 + 1.334 + public boolean inGuestMode() { 1.335 + return mInGuestMode; 1.336 + } 1.337 + 1.338 + private void setDir(File dir) { 1.339 + if (dir != null && dir.exists() && dir.isDirectory()) { 1.340 + mProfileDir = dir; 1.341 + } 1.342 + } 1.343 + 1.344 + public String getName() { 1.345 + return mName; 1.346 + } 1.347 + 1.348 + public synchronized File getDir() { 1.349 + forceCreate(); 1.350 + return mProfileDir; 1.351 + } 1.352 + 1.353 + public synchronized GeckoProfile forceCreate() { 1.354 + if (mProfileDir != null) { 1.355 + return this; 1.356 + } 1.357 + 1.358 + try { 1.359 + // Check if a profile with this name already exists. 1.360 + try { 1.361 + mProfileDir = findProfileDir(); 1.362 + Log.d(LOGTAG, "Found profile dir."); 1.363 + } catch (NoSuchProfileException noSuchProfile) { 1.364 + // If it doesn't exist, create it. 1.365 + mProfileDir = createProfileDir(); 1.366 + } 1.367 + } catch (IOException ioe) { 1.368 + Log.e(LOGTAG, "Error getting profile dir", ioe); 1.369 + } 1.370 + return this; 1.371 + } 1.372 + 1.373 + public File getFile(String aFile) { 1.374 + File f = getDir(); 1.375 + if (f == null) 1.376 + return null; 1.377 + 1.378 + return new File(f, aFile); 1.379 + } 1.380 + 1.381 + /** 1.382 + * Moves the session file to the backup session file. 1.383 + * 1.384 + * sessionstore.js should hold the current session, and sessionstore.bak 1.385 + * should hold the previous session (where it is used to read the "tabs 1.386 + * from last time"). Normally, sessionstore.js is moved to sessionstore.bak 1.387 + * on a clean quit, but this doesn't happen if Fennec crashed. Thus, this 1.388 + * method should be called after a crash so sessionstore.bak correctly 1.389 + * holds the previous session. 1.390 + */ 1.391 + public void moveSessionFile() { 1.392 + File sessionFile = getFile("sessionstore.js"); 1.393 + if (sessionFile != null && sessionFile.exists()) { 1.394 + File sessionFileBackup = getFile("sessionstore.bak"); 1.395 + sessionFile.renameTo(sessionFileBackup); 1.396 + } 1.397 + } 1.398 + 1.399 + /** 1.400 + * Get the string from a session file. 1.401 + * 1.402 + * The session can either be read from sessionstore.js or sessionstore.bak. 1.403 + * In general, sessionstore.js holds the current session, and 1.404 + * sessionstore.bak holds the previous session. 1.405 + * 1.406 + * @param readBackup if true, the session is read from sessionstore.bak; 1.407 + * otherwise, the session is read from sessionstore.js 1.408 + * 1.409 + * @return the session string 1.410 + */ 1.411 + public String readSessionFile(boolean readBackup) { 1.412 + File sessionFile = getFile(readBackup ? "sessionstore.bak" : "sessionstore.js"); 1.413 + 1.414 + try { 1.415 + if (sessionFile != null && sessionFile.exists()) { 1.416 + return readFile(sessionFile); 1.417 + } 1.418 + } catch (IOException ioe) { 1.419 + Log.e(LOGTAG, "Unable to read session file", ioe); 1.420 + } 1.421 + return null; 1.422 + } 1.423 + 1.424 + public String readFile(String filename) throws IOException { 1.425 + File dir = getDir(); 1.426 + if (dir == null) { 1.427 + throw new IOException("No profile directory found"); 1.428 + } 1.429 + File target = new File(dir, filename); 1.430 + return readFile(target); 1.431 + } 1.432 + 1.433 + private String readFile(File target) throws IOException { 1.434 + FileReader fr = new FileReader(target); 1.435 + try { 1.436 + StringBuilder sb = new StringBuilder(); 1.437 + char[] buf = new char[8192]; 1.438 + int read = fr.read(buf); 1.439 + while (read >= 0) { 1.440 + sb.append(buf, 0, read); 1.441 + read = fr.read(buf); 1.442 + } 1.443 + return sb.toString(); 1.444 + } finally { 1.445 + fr.close(); 1.446 + } 1.447 + } 1.448 + 1.449 + private boolean remove() { 1.450 + try { 1.451 + final File dir = getDir(); 1.452 + if (dir.exists()) { 1.453 + delete(dir); 1.454 + } 1.455 + 1.456 + try { 1.457 + mProfileDir = findProfileDir(); 1.458 + } catch (NoSuchProfileException noSuchProfile) { 1.459 + // If the profile doesn't exist, there's nothing left for us to do. 1.460 + return false; 1.461 + } 1.462 + 1.463 + final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir); 1.464 + final Hashtable<String, INISection> sections = parser.getSections(); 1.465 + for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) { 1.466 + final INISection section = e.nextElement(); 1.467 + String name = section.getStringProperty("Name"); 1.468 + 1.469 + if (name == null || !name.equals(mName)) { 1.470 + continue; 1.471 + } 1.472 + 1.473 + if (section.getName().startsWith("Profile")) { 1.474 + // ok, we have stupid Profile#-named things. Rename backwards. 1.475 + try { 1.476 + int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length())); 1.477 + String curSection = "Profile" + sectionNumber; 1.478 + String nextSection = "Profile" + (sectionNumber+1); 1.479 + 1.480 + sections.remove(curSection); 1.481 + 1.482 + while (sections.containsKey(nextSection)) { 1.483 + parser.renameSection(nextSection, curSection); 1.484 + sectionNumber++; 1.485 + 1.486 + curSection = nextSection; 1.487 + nextSection = "Profile" + (sectionNumber+1); 1.488 + } 1.489 + } catch (NumberFormatException nex) { 1.490 + // uhm, malformed Profile thing; we can't do much. 1.491 + Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName()); 1.492 + return false; 1.493 + } 1.494 + } else { 1.495 + // this really shouldn't be the case, but handle it anyway 1.496 + parser.removeSection(mName); 1.497 + } 1.498 + 1.499 + break; 1.500 + } 1.501 + 1.502 + parser.write(); 1.503 + return true; 1.504 + } catch (IOException ex) { 1.505 + Log.w(LOGTAG, "Failed to remove profile.", ex); 1.506 + return false; 1.507 + } 1.508 + } 1.509 + 1.510 + /** 1.511 + * @return the default profile name for this application, or 1.512 + * {@link GeckoProfile#DEFAULT_PROFILE} if none could be found. 1.513 + * 1.514 + * @throws NoMozillaDirectoryException 1.515 + * if the Mozilla directory did not exist and could not be 1.516 + * created. 1.517 + */ 1.518 + public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException { 1.519 + // Have we read the default profile from the INI already? 1.520 + // Changing the default profile requires a restart, so we don't 1.521 + // need to worry about runtime changes. 1.522 + if (sDefaultProfileName != null) { 1.523 + return sDefaultProfileName; 1.524 + } 1.525 + 1.526 + final String profileName = GeckoProfileDirectories.findDefaultProfileName(context); 1.527 + if (profileName == null) { 1.528 + // Note that we don't persist this back to profiles.ini. 1.529 + sDefaultProfileName = DEFAULT_PROFILE; 1.530 + return DEFAULT_PROFILE; 1.531 + } 1.532 + 1.533 + sDefaultProfileName = profileName; 1.534 + return sDefaultProfileName; 1.535 + } 1.536 + 1.537 + private File findProfileDir() throws NoSuchProfileException { 1.538 + return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName); 1.539 + } 1.540 + 1.541 + private File createProfileDir() throws IOException { 1.542 + INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir); 1.543 + 1.544 + // Salt the name of our requested profile 1.545 + String saltedName = GeckoProfileDirectories.saltProfileName(mName); 1.546 + File profileDir = new File(mMozillaDir, saltedName); 1.547 + while (profileDir.exists()) { 1.548 + saltedName = GeckoProfileDirectories.saltProfileName(mName); 1.549 + profileDir = new File(mMozillaDir, saltedName); 1.550 + } 1.551 + 1.552 + // Attempt to create the salted profile dir 1.553 + if (!profileDir.mkdirs()) { 1.554 + throw new IOException("Unable to create profile."); 1.555 + } 1.556 + Log.d(LOGTAG, "Created new profile dir."); 1.557 + 1.558 + // Now update profiles.ini 1.559 + // If this is the first time its created, we also add a General section 1.560 + // look for the first profile number that isn't taken yet 1.561 + int profileNum = 0; 1.562 + boolean isDefaultSet = false; 1.563 + INISection profileSection; 1.564 + while ((profileSection = parser.getSection("Profile" + profileNum)) != null) { 1.565 + profileNum++; 1.566 + if (profileSection.getProperty("Default") != null) { 1.567 + isDefaultSet = true; 1.568 + } 1.569 + } 1.570 + 1.571 + profileSection = new INISection("Profile" + profileNum); 1.572 + profileSection.setProperty("Name", mName); 1.573 + profileSection.setProperty("IsRelative", 1); 1.574 + profileSection.setProperty("Path", saltedName); 1.575 + 1.576 + if (parser.getSection("General") == null) { 1.577 + INISection generalSection = new INISection("General"); 1.578 + generalSection.setProperty("StartWithLastProfile", 1); 1.579 + parser.addSection(generalSection); 1.580 + } 1.581 + 1.582 + if (!isDefaultSet && !mName.startsWith("webapp")) { 1.583 + // only set as default if this is the first non-webapp 1.584 + // profile we're creating 1.585 + profileSection.setProperty("Default", 1); 1.586 + 1.587 + // We have no intention of stopping this session. The FIRSTRUN session 1.588 + // ends when the browsing session/activity has ended. All events 1.589 + // during firstrun will be tagged as FIRSTRUN. 1.590 + Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN); 1.591 + } 1.592 + 1.593 + parser.addSection(profileSection); 1.594 + parser.write(); 1.595 + 1.596 + // Write out profile creation time, mirroring the logic in nsToolkitProfileService. 1.597 + try { 1.598 + FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + "times.json"); 1.599 + OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8")); 1.600 + try { 1.601 + writer.append("{\"created\": " + System.currentTimeMillis() + "}\n"); 1.602 + } finally { 1.603 + writer.close(); 1.604 + } 1.605 + } catch (Exception e) { 1.606 + // Best-effort. 1.607 + Log.w(LOGTAG, "Couldn't write times.json.", e); 1.608 + } 1.609 + 1.610 + return profileDir; 1.611 + } 1.612 +}