mobile/android/base/GeckoProfile.java

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:69746c48d28a
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/. */
5
6 package org.mozilla.gecko;
7
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;
14
15 import android.content.Context;
16 import android.text.TextUtils;
17 import android.util.Log;
18
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;
28
29 public final class GeckoProfile {
30 private static final String LOGTAG = "GeckoProfile";
31
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";
36
37 private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
38 private static String sDefaultProfileName = null;
39
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.
44
45 // Constants to cache whether or not a profile is "locked".
46 private enum LockState {
47 LOCKED,
48 UNLOCKED,
49 UNDEFINED
50 };
51
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;
55
56 // Caches the guest profile dir.
57 private static File sGuestDir = null;
58 private static GeckoProfile sGuestProfile = null;
59
60 private boolean mInGuestMode = false;
61
62
63 public static GeckoProfile get(Context context) {
64 boolean isGeckoApp = false;
65 try {
66 isGeckoApp = context instanceof GeckoApp;
67 } catch (NoClassDefFoundError ex) {}
68
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 }
77
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 }
83
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 }
98
99 return get(context, "");
100 }
101
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 }
110
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 }
121
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 }
126
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 }
137
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 }
156
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 }
165
166 if (success) {
167 // Clear all shared prefs for the given profile.
168 GeckoSharedPrefs.forProfileName(context, profileName)
169 .edit().clear().commit();
170 }
171
172 return success;
173 }
174
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 }
189
190 public static void leaveGuestSession(Context context) {
191 GeckoProfile profile = getGuestProfile(context);
192 if (profile != null) {
193 profile.unlock();
194 }
195 }
196
197 private static File getGuestDir(Context context) {
198 if (sGuestDir == null) {
199 sGuestDir = context.getFileStreamPath("guest");
200 }
201 return sGuestDir;
202 }
203
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 }
212
213 return sGuestProfile;
214 }
215
216 public static boolean maybeCleanupGuestProfile(final Context context) {
217 final GeckoProfile profile = getGuestProfile(context);
218
219 if (profile == null) {
220 return false;
221 } else if (!profile.locked()) {
222 profile.mInGuestMode = false;
223
224 // If the guest dir exists, but it's unlocked, delete it
225 removeGuestProfile(context);
226
227 return true;
228 }
229 return false;
230 }
231
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 }
242
243 if (success) {
244 // Clear all shared prefs for the guest profile.
245 GeckoSharedPrefs.forProfileName(context, GUEST_PROFILE)
246 .edit().clear().commit();
247 }
248 }
249
250 public static boolean delete(File file) throws IOException {
251 // Try to do a quick initial delete
252 if (file.delete())
253 return true;
254
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 }
263
264 // Even if this is a dir, it should now be empty and delete should work
265 return file.delete();
266 }
267
268 private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException {
269 mName = profileName;
270 mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
271 }
272
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 }
278
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 }
287
288 return mLocked == LockState.LOCKED;
289 }
290
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 }
308
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 }
314
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 }
330
331 public boolean inGuestMode() {
332 return mInGuestMode;
333 }
334
335 private void setDir(File dir) {
336 if (dir != null && dir.exists() && dir.isDirectory()) {
337 mProfileDir = dir;
338 }
339 }
340
341 public String getName() {
342 return mName;
343 }
344
345 public synchronized File getDir() {
346 forceCreate();
347 return mProfileDir;
348 }
349
350 public synchronized GeckoProfile forceCreate() {
351 if (mProfileDir != null) {
352 return this;
353 }
354
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 }
369
370 public File getFile(String aFile) {
371 File f = getDir();
372 if (f == null)
373 return null;
374
375 return new File(f, aFile);
376 }
377
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 }
395
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");
410
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 }
420
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 }
429
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 }
445
446 private boolean remove() {
447 try {
448 final File dir = getDir();
449 if (dir.exists()) {
450 delete(dir);
451 }
452
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 }
459
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");
465
466 if (name == null || !name.equals(mName)) {
467 continue;
468 }
469
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);
476
477 sections.remove(curSection);
478
479 while (sections.containsKey(nextSection)) {
480 parser.renameSection(nextSection, curSection);
481 sectionNumber++;
482
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 }
495
496 break;
497 }
498
499 parser.write();
500 return true;
501 } catch (IOException ex) {
502 Log.w(LOGTAG, "Failed to remove profile.", ex);
503 return false;
504 }
505 }
506
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 }
522
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 }
529
530 sDefaultProfileName = profileName;
531 return sDefaultProfileName;
532 }
533
534 private File findProfileDir() throws NoSuchProfileException {
535 return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
536 }
537
538 private File createProfileDir() throws IOException {
539 INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
540
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 }
548
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.");
554
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 }
567
568 profileSection = new INISection("Profile" + profileNum);
569 profileSection.setProperty("Name", mName);
570 profileSection.setProperty("IsRelative", 1);
571 profileSection.setProperty("Path", saltedName);
572
573 if (parser.getSection("General") == null) {
574 INISection generalSection = new INISection("General");
575 generalSection.setProperty("StartWithLastProfile", 1);
576 parser.addSection(generalSection);
577 }
578
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);
583
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 }
589
590 parser.addSection(profileSection);
591 parser.write();
592
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 }
606
607 return profileDir;
608 }
609 }

mercurial