|
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 } |