mobile/android/base/GeckoProfile.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko;
     8 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
     9 import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
    10 import org.mozilla.gecko.Telemetry;
    11 import org.mozilla.gecko.TelemetryContract;
    12 import org.mozilla.gecko.util.INIParser;
    13 import org.mozilla.gecko.util.INISection;
    15 import android.content.Context;
    16 import android.text.TextUtils;
    17 import android.util.Log;
    19 import java.io.File;
    20 import java.io.FileOutputStream;
    21 import java.io.FileReader;
    22 import java.io.IOException;
    23 import java.io.OutputStreamWriter;
    24 import java.nio.charset.Charset;
    25 import java.util.Enumeration;
    26 import java.util.HashMap;
    27 import java.util.Hashtable;
    29 public final class GeckoProfile {
    30     private static final String LOGTAG = "GeckoProfile";
    32     // Used to "lock" the guest profile, so that we'll always restart in it
    33     private static final String LOCK_FILE_NAME = ".active_lock";
    34     public static final String DEFAULT_PROFILE = "default";
    35     private static final String GUEST_PROFILE = "guest";
    37     private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
    38     private static String sDefaultProfileName = null;
    40     public static boolean sIsUsingCustomProfile = false;
    41     private final String mName;
    42     private final File mMozillaDir;
    43     private File mProfileDir;             // Not final because this is lazily computed.
    45     // Constants to cache whether or not a profile is "locked".
    46     private enum LockState {
    47         LOCKED,
    48         UNLOCKED,
    49         UNDEFINED
    50     };
    52     // Caches whether or not a profile is "locked". Only used by the guest profile to determine if it should
    53     // be reused or deleted on startup
    54     private LockState mLocked = LockState.UNDEFINED;
    56     // Caches the guest profile dir.
    57     private static File sGuestDir = null;
    58     private static GeckoProfile sGuestProfile = null;
    60     private boolean mInGuestMode = false;
    63     public static GeckoProfile get(Context context) {
    64         boolean isGeckoApp = false;
    65         try {
    66             isGeckoApp = context instanceof GeckoApp;
    67         } catch (NoClassDefFoundError ex) {}
    69         if (isGeckoApp) {
    70             // Check for a cached profile on this context already
    71             // TODO: We should not be caching profile information on the Activity context
    72             final GeckoApp geckoApp = (GeckoApp) context;
    73             if (geckoApp.mProfile != null) {
    74                 return geckoApp.mProfile;
    75             }
    76         }
    78         // If the guest profile exists and is locked, return it
    79         GeckoProfile guest = GeckoProfile.getGuestProfile(context);
    80         if (guest != null && guest.locked()) {
    81             return guest;
    82         }
    84         if (isGeckoApp) {
    85             final GeckoApp geckoApp = (GeckoApp) context;
    86             String defaultProfileName;
    87             try {
    88                 defaultProfileName = geckoApp.getDefaultProfileName();
    89             } catch (NoMozillaDirectoryException e) {
    90                 // If this failed, we're screwed. But there are so many callers that
    91                 // we'll just throw a RuntimeException.
    92                 Log.wtf(LOGTAG, "Unable to get default profile name.", e);
    93                 throw new RuntimeException(e);
    94             }
    95             // Otherwise, get the default profile for the Activity.
    96             return get(context, defaultProfileName);
    97         }
    99         return get(context, "");
   100     }
   102     public static GeckoProfile get(Context context, String profileName) {
   103         synchronized (sProfileCache) {
   104             GeckoProfile profile = sProfileCache.get(profileName);
   105             if (profile != null)
   106                 return profile;
   107         }
   108         return get(context, profileName, (File)null);
   109     }
   111     public static GeckoProfile get(Context context, String profileName, String profilePath) {
   112         File dir = null;
   113         if (!TextUtils.isEmpty(profilePath)) {
   114             dir = new File(profilePath);
   115             if (!dir.exists() || !dir.isDirectory()) {
   116                 Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
   117             }
   118         }
   119         return get(context, profileName, dir);
   120     }
   122     public static GeckoProfile get(Context context, String profileName, File profileDir) {
   123         if (context == null) {
   124             throw new IllegalArgumentException("context must be non-null");
   125         }
   127         // if no profile was passed in, look for the default profile listed in profiles.ini
   128         // if that doesn't exist, look for a profile called 'default'
   129         if (TextUtils.isEmpty(profileName) && profileDir == null) {
   130             try {
   131                 profileName = GeckoProfile.getDefaultProfileName(context);
   132             } catch (NoMozillaDirectoryException e) {
   133                 // We're unable to do anything sane here.
   134                 throw new RuntimeException(e);
   135             }
   136         }
   138         // actually try to look up the profile
   139         synchronized (sProfileCache) {
   140             GeckoProfile profile = sProfileCache.get(profileName);
   141             if (profile == null) {
   142                 try {
   143                     profile = new GeckoProfile(context, profileName);
   144                 } catch (NoMozillaDirectoryException e) {
   145                     // We're unable to do anything sane here.
   146                     throw new RuntimeException(e);
   147                 }
   148                 profile.setDir(profileDir);
   149                 sProfileCache.put(profileName, profile);
   150             } else {
   151                 profile.setDir(profileDir);
   152             }
   153             return profile;
   154         }
   155     }
   157     public static boolean removeProfile(Context context, String profileName) {
   158         final boolean success;
   159         try {
   160             success = new GeckoProfile(context, profileName).remove();
   161         } catch (NoMozillaDirectoryException e) {
   162             Log.w(LOGTAG, "Unable to remove profile: no Mozilla directory.", e);
   163             return true;
   164         }
   166         if (success) {
   167             // Clear all shared prefs for the given profile.
   168             GeckoSharedPrefs.forProfileName(context, profileName)
   169                             .edit().clear().commit();
   170         }
   172         return success;
   173     }
   175     public static GeckoProfile createGuestProfile(Context context) {
   176         try {
   177             removeGuestProfile(context);
   178             // We need to force the creation of a new guest profile if we want it outside of the normal profile path,
   179             // otherwise GeckoProfile.getDir will try to be smart and build it for us in the normal profiles dir.
   180             getGuestDir(context).mkdir();
   181             GeckoProfile profile = getGuestProfile(context);
   182             profile.lock();
   183             return profile;
   184         } catch (Exception ex) {
   185             Log.e(LOGTAG, "Error creating guest profile", ex);
   186         }
   187         return null;
   188     }
   190     public static void leaveGuestSession(Context context) {
   191         GeckoProfile profile = getGuestProfile(context);
   192         if (profile != null) {
   193             profile.unlock();
   194         }
   195     }
   197     private static File getGuestDir(Context context) {
   198         if (sGuestDir == null) {
   199             sGuestDir = context.getFileStreamPath("guest");
   200         }
   201         return sGuestDir;
   202     }
   204     private static GeckoProfile getGuestProfile(Context context) {
   205         if (sGuestProfile == null) {
   206             File guestDir = getGuestDir(context);
   207             if (guestDir.exists()) {
   208                 sGuestProfile = get(context, GUEST_PROFILE, guestDir);
   209                 sGuestProfile.mInGuestMode = true;
   210             }
   211         }
   213         return sGuestProfile;
   214     }
   216     public static boolean maybeCleanupGuestProfile(final Context context) {
   217         final GeckoProfile profile = getGuestProfile(context);
   219         if (profile == null) {
   220             return false;
   221         } else if (!profile.locked()) {
   222             profile.mInGuestMode = false;
   224             // If the guest dir exists, but it's unlocked, delete it
   225             removeGuestProfile(context);
   227             return true;
   228         }
   229         return false;
   230     }
   232     private static void removeGuestProfile(Context context) {
   233         boolean success = false;
   234         try {
   235             File guestDir = getGuestDir(context);
   236             if (guestDir.exists()) {
   237                 success = delete(guestDir);
   238             }
   239         } catch (Exception ex) {
   240             Log.e(LOGTAG, "Error removing guest profile", ex);
   241         }
   243         if (success) {
   244             // Clear all shared prefs for the guest profile.
   245             GeckoSharedPrefs.forProfileName(context, GUEST_PROFILE)
   246                             .edit().clear().commit();
   247         }
   248     }
   250     public static boolean delete(File file) throws IOException {
   251         // Try to do a quick initial delete
   252         if (file.delete())
   253             return true;
   255         if (file.isDirectory()) {
   256             // If the quick delete failed and this is a dir, recursively delete the contents of the dir
   257             String files[] = file.list();
   258             for (String temp : files) {
   259                 File fileDelete = new File(file, temp);
   260                 delete(fileDelete);
   261             }
   262         }
   264         // Even if this is a dir, it should now be empty and delete should work
   265         return file.delete();
   266     }
   268     private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException {
   269         mName = profileName;
   270         mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
   271     }
   273     // Warning, Changing the lock file state from outside apis will cause this to become out of sync
   274     public boolean locked() {
   275         if (mLocked != LockState.UNDEFINED) {
   276             return mLocked == LockState.LOCKED;
   277         }
   279         // Don't use getDir() as it will create a dir if none exists
   280         if (mProfileDir != null && mProfileDir.exists()) {
   281             File lockFile = new File(mProfileDir, LOCK_FILE_NAME);
   282             boolean res = lockFile.exists();
   283             mLocked = res ? LockState.LOCKED : LockState.UNLOCKED;
   284         } else {
   285             mLocked = LockState.UNLOCKED;
   286         }
   288         return mLocked == LockState.LOCKED;
   289     }
   291     public boolean lock() {
   292         try {
   293             // If this dir doesn't exist getDir will create it for us
   294             File lockFile = new File(getDir(), LOCK_FILE_NAME);
   295             boolean result = lockFile.createNewFile();
   296             if (result) {
   297                 mLocked = LockState.LOCKED;
   298             } else {
   299                 mLocked = LockState.UNLOCKED;
   300             }
   301             return result;
   302         } catch(IOException ex) {
   303             Log.e(LOGTAG, "Error locking profile", ex);
   304         }
   305         mLocked = LockState.UNLOCKED;
   306         return false;
   307     }
   309     public boolean unlock() {
   310         // Don't use getDir() as it will create a dir
   311         if (mProfileDir == null || !mProfileDir.exists()) {
   312             return true;
   313         }
   315         try {
   316             File lockFile = new File(mProfileDir, LOCK_FILE_NAME);
   317             boolean result = delete(lockFile);
   318             if (result) {
   319                 mLocked = LockState.UNLOCKED;
   320             } else {
   321                 mLocked = LockState.LOCKED;
   322             }
   323             return result;
   324         } catch(IOException ex) {
   325             Log.e(LOGTAG, "Error unlocking profile", ex);
   326         }
   327         mLocked = LockState.LOCKED;
   328         return false;
   329     }
   331     public boolean inGuestMode() {
   332         return mInGuestMode;
   333     }
   335     private void setDir(File dir) {
   336         if (dir != null && dir.exists() && dir.isDirectory()) {
   337             mProfileDir = dir;
   338         }
   339     }
   341     public String getName() {
   342         return mName;
   343     }
   345     public synchronized File getDir() {
   346         forceCreate();
   347         return mProfileDir;
   348     }
   350     public synchronized GeckoProfile forceCreate() {
   351         if (mProfileDir != null) {
   352             return this;
   353         }
   355         try {
   356             // Check if a profile with this name already exists.
   357             try {
   358                 mProfileDir = findProfileDir();
   359                 Log.d(LOGTAG, "Found profile dir.");
   360             } catch (NoSuchProfileException noSuchProfile) {
   361                 // If it doesn't exist, create it.
   362                 mProfileDir = createProfileDir();
   363             }
   364         } catch (IOException ioe) {
   365             Log.e(LOGTAG, "Error getting profile dir", ioe);
   366         }
   367         return this;
   368     }
   370     public File getFile(String aFile) {
   371         File f = getDir();
   372         if (f == null)
   373             return null;
   375         return new File(f, aFile);
   376     }
   378     /**
   379      * Moves the session file to the backup session file.
   380      *
   381      * sessionstore.js should hold the current session, and sessionstore.bak
   382      * should hold the previous session (where it is used to read the "tabs
   383      * from last time"). Normally, sessionstore.js is moved to sessionstore.bak
   384      * on a clean quit, but this doesn't happen if Fennec crashed. Thus, this
   385      * method should be called after a crash so sessionstore.bak correctly
   386      * holds the previous session.
   387      */
   388     public void moveSessionFile() {
   389         File sessionFile = getFile("sessionstore.js");
   390         if (sessionFile != null && sessionFile.exists()) {
   391             File sessionFileBackup = getFile("sessionstore.bak");
   392             sessionFile.renameTo(sessionFileBackup);
   393         }
   394     }
   396     /**
   397      * Get the string from a session file.
   398      *
   399      * The session can either be read from sessionstore.js or sessionstore.bak.
   400      * In general, sessionstore.js holds the current session, and
   401      * sessionstore.bak holds the previous session.
   402      *
   403      * @param readBackup if true, the session is read from sessionstore.bak;
   404      *                   otherwise, the session is read from sessionstore.js
   405      *
   406      * @return the session string
   407      */
   408     public String readSessionFile(boolean readBackup) {
   409         File sessionFile = getFile(readBackup ? "sessionstore.bak" : "sessionstore.js");
   411         try {
   412             if (sessionFile != null && sessionFile.exists()) {
   413                 return readFile(sessionFile);
   414             }
   415         } catch (IOException ioe) {
   416             Log.e(LOGTAG, "Unable to read session file", ioe);
   417         }
   418         return null;
   419     }
   421     public String readFile(String filename) throws IOException {
   422         File dir = getDir();
   423         if (dir == null) {
   424             throw new IOException("No profile directory found");
   425         }
   426         File target = new File(dir, filename);
   427         return readFile(target);
   428     }
   430     private String readFile(File target) throws IOException {
   431         FileReader fr = new FileReader(target);
   432         try {
   433             StringBuilder sb = new StringBuilder();
   434             char[] buf = new char[8192];
   435             int read = fr.read(buf);
   436             while (read >= 0) {
   437                 sb.append(buf, 0, read);
   438                 read = fr.read(buf);
   439             }
   440             return sb.toString();
   441         } finally {
   442             fr.close();
   443         }
   444     }
   446     private boolean remove() {
   447         try {
   448             final File dir = getDir();
   449             if (dir.exists()) {
   450                 delete(dir);
   451             }
   453             try {
   454                 mProfileDir = findProfileDir();
   455             } catch (NoSuchProfileException noSuchProfile) {
   456                 // If the profile doesn't exist, there's nothing left for us to do.
   457                 return false;
   458             }
   460             final INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
   461             final Hashtable<String, INISection> sections = parser.getSections();
   462             for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
   463                 final INISection section = e.nextElement();
   464                 String name = section.getStringProperty("Name");
   466                 if (name == null || !name.equals(mName)) {
   467                     continue;
   468                 }
   470                 if (section.getName().startsWith("Profile")) {
   471                     // ok, we have stupid Profile#-named things.  Rename backwards.
   472                     try {
   473                         int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
   474                         String curSection = "Profile" + sectionNumber;
   475                         String nextSection = "Profile" + (sectionNumber+1);
   477                         sections.remove(curSection);
   479                         while (sections.containsKey(nextSection)) {
   480                             parser.renameSection(nextSection, curSection);
   481                             sectionNumber++;
   483                             curSection = nextSection;
   484                             nextSection = "Profile" + (sectionNumber+1);
   485                         }
   486                     } catch (NumberFormatException nex) {
   487                         // uhm, malformed Profile thing; we can't do much.
   488                         Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
   489                         return false;
   490                     }
   491                 } else {
   492                     // this really shouldn't be the case, but handle it anyway
   493                     parser.removeSection(mName);
   494                 }
   496                 break;
   497             }
   499             parser.write();
   500             return true;
   501         } catch (IOException ex) {
   502             Log.w(LOGTAG, "Failed to remove profile.", ex);
   503             return false;
   504         }
   505     }
   507     /**
   508      * @return the default profile name for this application, or
   509      *         {@link GeckoProfile#DEFAULT_PROFILE} if none could be found.
   510      *
   511      * @throws NoMozillaDirectoryException
   512      *             if the Mozilla directory did not exist and could not be
   513      *             created.
   514      */
   515     public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
   516         // Have we read the default profile from the INI already?
   517         // Changing the default profile requires a restart, so we don't
   518         // need to worry about runtime changes.
   519         if (sDefaultProfileName != null) {
   520             return sDefaultProfileName;
   521         }
   523         final String profileName = GeckoProfileDirectories.findDefaultProfileName(context);
   524         if (profileName == null) {
   525             // Note that we don't persist this back to profiles.ini.
   526             sDefaultProfileName = DEFAULT_PROFILE;
   527             return DEFAULT_PROFILE;
   528         }
   530         sDefaultProfileName = profileName;
   531         return sDefaultProfileName;
   532     }
   534     private File findProfileDir() throws NoSuchProfileException {
   535         return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
   536     }
   538     private File createProfileDir() throws IOException {
   539         INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
   541         // Salt the name of our requested profile
   542         String saltedName = GeckoProfileDirectories.saltProfileName(mName);
   543         File profileDir = new File(mMozillaDir, saltedName);
   544         while (profileDir.exists()) {
   545             saltedName = GeckoProfileDirectories.saltProfileName(mName);
   546             profileDir = new File(mMozillaDir, saltedName);
   547         }
   549         // Attempt to create the salted profile dir
   550         if (!profileDir.mkdirs()) {
   551             throw new IOException("Unable to create profile.");
   552         }
   553         Log.d(LOGTAG, "Created new profile dir.");
   555         // Now update profiles.ini
   556         // If this is the first time its created, we also add a General section
   557         // look for the first profile number that isn't taken yet
   558         int profileNum = 0;
   559         boolean isDefaultSet = false;
   560         INISection profileSection;
   561         while ((profileSection = parser.getSection("Profile" + profileNum)) != null) {
   562             profileNum++;
   563             if (profileSection.getProperty("Default") != null) {
   564                 isDefaultSet = true;
   565             }
   566         }
   568         profileSection = new INISection("Profile" + profileNum);
   569         profileSection.setProperty("Name", mName);
   570         profileSection.setProperty("IsRelative", 1);
   571         profileSection.setProperty("Path", saltedName);
   573         if (parser.getSection("General") == null) {
   574             INISection generalSection = new INISection("General");
   575             generalSection.setProperty("StartWithLastProfile", 1);
   576             parser.addSection(generalSection);
   577         }
   579         if (!isDefaultSet && !mName.startsWith("webapp")) {
   580             // only set as default if this is the first non-webapp
   581             // profile we're creating
   582             profileSection.setProperty("Default", 1);
   584             // We have no intention of stopping this session. The FIRSTRUN session
   585             // ends when the browsing session/activity has ended. All events
   586             // during firstrun will be tagged as FIRSTRUN.
   587             Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
   588         }
   590         parser.addSection(profileSection);
   591         parser.write();
   593         // Write out profile creation time, mirroring the logic in nsToolkitProfileService.
   594         try {
   595             FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + "times.json");
   596             OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
   597             try {
   598                 writer.append("{\"created\": " + System.currentTimeMillis() + "}\n");
   599             } finally {
   600                 writer.close();
   601             }
   602         } catch (Exception e) {
   603             // Best-effort.
   604             Log.w(LOGTAG, "Couldn't write times.json.", e);
   605         }
   607         return profileDir;
   608     }
   609 }

mercurial