Bug 722661 - Part 2: Stub tabs in Java when doing an OOM restore. r=mfinkle
authorBrian Nicholson <bnicholson@mozilla.com>
Mon, 29 Oct 2012 16:34:29 -0700
changeset 111849 a9579b178f216fae79e3d151c40303c5e6e770e3
parent 111848 376802b2671da2896b3c6847f24fcc81d52d8fdd
child 111850 619ac2cecd1e2b3b30d6779ad925bc4ffe83cfca
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersmfinkle
bugs722661
milestone19.0a1
Bug 722661 - Part 2: Stub tabs in Java when doing an OOM restore. r=mfinkle
mobile/android/app/mobile.js
mobile/android/base/AboutHomeContent.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoProfile.java
mobile/android/base/SessionParser.java
mobile/android/base/Tabs.java
mobile/android/chrome/content/browser.js
mobile/android/components/SessionStore.idl
mobile/android/components/SessionStore.js
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -102,18 +102,17 @@ pref("browser.display.history.maxresults
 pref("browser.display.remotetabs.timeout", 10);
 
 /* session history */
 pref("browser.sessionhistory.max_total_viewers", 1);
 pref("browser.sessionhistory.max_entries", 50);
 
 /* session store */
 pref("browser.sessionstore.resume_session_once", false);
-pref("browser.sessionstore.resume_from_crash", false);
-pref("browser.sessionstore.resume_from_crash_timeout", 60); // minutes
+pref("browser.sessionstore.resume_from_crash", true);
 pref("browser.sessionstore.interval", 10000); // milliseconds
 pref("browser.sessionstore.max_tabs_undo", 1);
 pref("browser.sessionstore.max_resumed_crashes", 1);
 pref("browser.sessionstore.recent_crashes", 0);
 
 /* these should help performance */
 pref("mozilla.widget.force-24bpp", true);
 pref("mozilla.widget.use-buffer-pixmap", true);
--- a/mobile/android/base/AboutHomeContent.java
+++ b/mobile/android/base/AboutHomeContent.java
@@ -458,17 +458,17 @@ public class AboutHomeContent extends Sc
                 } catch (JSONException e) {
                     Log.i(LOGTAG, "error reading json file", e);
                 }
             }
         });
     }
 
     private void readLastTabs() {
-        String jsonString = mActivity.getProfile().readSessionFile(GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning));
+        String jsonString = mActivity.getProfile().readSessionFile(true);
         if (jsonString == null) {
             // no previous session data
             return;
         }
 
         final ArrayList<String> lastTabUrlsList = new ArrayList<String>();
         new SessionParser() {
             @Override
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1589,17 +1589,17 @@ abstract public class GeckoApp
         if (uri != null && uri.length() > 0) {
             passedUri = uri;
         }
 
         if (mRestoreMode == GeckoAppShell.RESTORE_NONE && getProfile().shouldRestoreSession()) {
             mRestoreMode = GeckoAppShell.RESTORE_CRASH;
         }
 
-        boolean isExternalURL = passedUri != null && !passedUri.equals("about:home");
+        final boolean isExternalURL = passedUri != null && !passedUri.equals("about:home");
         StartupAction startupAction;
         if (isExternalURL) {
             startupAction = StartupAction.URL;
         } else {
             startupAction = StartupAction.NORMAL;
         }
 
         // Start migrating as early as possible, can do this in
@@ -1622,20 +1622,79 @@ abstract public class GeckoApp
             } else {
                 startupAction = StartupAction.PREFETCH;
                 GeckoAppShell.getHandler().post(new PrefetchRunnable(copy));
             }
         }
 
         Tabs.registerOnTabsChangedListener(this);
 
+        // If we are doing a restore, read the session data and send it to Gecko
+        if (mRestoreMode != GeckoAppShell.RESTORE_NONE) {
+            try {
+                String sessionString = getProfile().readSessionFile(false);
+                if (sessionString == null) {
+                    throw new IOException("could not read from session file");
+                }
+
+                // If we are doing an OOM restore, parse the session data and
+                // stub the restored tabs immediately. This allows the UI to be
+                // updated before Gecko has restored.
+                if (mRestoreMode == GeckoAppShell.RESTORE_OOM) {
+                    final JSONArray tabs = new JSONArray();
+                    SessionParser parser = new SessionParser() {
+                        @Override
+                        public void onTabRead(SessionTab sessionTab) {
+                            JSONObject tabObject = sessionTab.getTabObject();
+
+                            int flags = Tabs.LOADURL_NEW_TAB;
+                            flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0);
+                            flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
+
+                            Tab tab = Tabs.getInstance().loadUrl(sessionTab.getSelectedUrl(), flags);
+                            tab.updateTitle(sessionTab.getSelectedTitle());
+
+                            try {
+                                tabObject.put("tabId", tab.getId());
+                            } catch (JSONException e) {
+                                Log.e(LOGTAG, "JSON error", e);
+                            }
+                            tabs.put(tabObject);
+                        }
+                    };
+
+                    parser.parse(sessionString);
+
+                    if (tabs.length() > 0) {
+                        sessionString = new JSONObject().put("windows", new JSONArray().put(new JSONObject().put("tabs", tabs))).toString();
+                    } else {
+                        throw new Exception("No tabs could be read from session file");
+                    }
+                }
+
+                JSONObject restoreData = new JSONObject();
+                restoreData.put("restoringOOM", mRestoreMode == GeckoAppShell.RESTORE_OOM);
+                restoreData.put("sessionString", sessionString);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Restore", restoreData.toString()));
+            } catch (Exception e) {
+                // If restore failed, do a normal startup
+                Log.e(LOGTAG, "An error occurred during restore", e);
+                mRestoreMode = GeckoAppShell.RESTORE_NONE;
+            }
+        }
+
+        // Move the session file if it exists
+        if (mRestoreMode != GeckoAppShell.RESTORE_OOM) {
+            getProfile().moveSessionFile();
+        }
+
         initializeChrome(passedUri, isExternalURL);
 
+        // Show telemetry door hanger if we aren't restoring a session
         if (mRestoreMode == GeckoAppShell.RESTORE_NONE) {
-            // show telemetry door hanger if we aren't restoring a session
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Telemetry:Prompt", null));
         }
 
         Telemetry.HistogramAdd("FENNEC_STARTUP_GECKOAPP_ACTION", startupAction.ordinal());
 
         if (!mIsRestoringActivity) {
             sGeckoThread = new GeckoThread(intent, passedUri, mRestoreMode);
         }
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -176,60 +176,78 @@ public final class GeckoProfile {
 
         return new File(f, aFile);
     }
 
     public File getFilesDir() {
         return mContext.getFilesDir();
     }
 
+    /**
+     * Determines whether the tabs from the previous session should be
+     * automatically restored.
+     *
+     * sessionstore.js is moved to sessionstore.bak on a clean quit, so if we
+     * still have sessionstore.js at startup, that means we were killed
+     * uncleanly. This is caused by either 1) a crash, or 2) being killed by
+     * android because of memory constraints. Either way, the existence of this
+     * file indicates that we'll want to restore the previous session.
+     *
+     * @return whether the previous session should be restored
+     */
     public boolean shouldRestoreSession() {
-        Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - start check sessionstore.js exists");
-        File dir = getDir();
-        if (dir == null)
-            return false;
-
-        File sessionFile = new File(dir, "sessionstore.js");
-        if (!sessionFile.exists())
+        File sessionFile = getFile("sessionstore.js");
+        if (sessionFile == null || !sessionFile.exists())
             return false;
 
         boolean shouldRestore = (System.currentTimeMillis() - sessionFile.lastModified() < SESSION_TIMEOUT);
-        Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - finish check sessionstore.js exists");
         return shouldRestore;
     }
 
-    public String readSessionFile(boolean geckoReady) {
-        File dir = getDir();
-        if (dir == null) {
-            return null;
+    /**
+     * Moves the session file to the backup session file.
+     *
+     * sessionstore.js should hold the current session, and sessionstore.bak
+     * should hold the previous session (where it is used to read the "tabs
+     * from last time"). Normally, sessionstore.js is moved to sessionstore.bak
+     * on a clean quit, but this doesn't happen if Fennec crashed. Thus, this
+     * method should be called after a crash so sessionstore.bak correctly
+     * holds the previous session.
+     */
+    public void moveSessionFile() {
+        File sessionFile = getFile("sessionstore.js");
+        if (sessionFile != null && sessionFile.exists()) {
+            File sessionFileBackup = getFile("sessionstore.bak");
+            sessionFile.renameTo(sessionFileBackup);
         }
+    }
 
-        File sessionFile = null;
-        if (! geckoReady) {
-            // we might have crashed, in which case sessionstore.js has tabs from last time
-            sessionFile = new File(dir, "sessionstore.js");
-            if (! sessionFile.exists()) {
-                sessionFile = null;
-            }
-        }
-        if (sessionFile == null) {
-            // either we did not crash, so previous session was moved to sessionstore.bak on quit,
-            // or sessionstore init has occurred, so previous session will always
-            // be in sessionstore.bak
-            sessionFile = new File(dir, "sessionstore.bak");
-            // no need to check if the session file exists here; readFile will throw
-            // an IOException if it does not
-        }
+    /**
+     * Get the string from a session file.
+     *
+     * The session can either be read from sessionstore.js or sessionstore.bak.
+     * In general, sessionstore.js holds the current session, and
+     * sessionstore.bak holds the previous session.
+     *
+     * @param readBackup if true, the session is read from sessionstore.bak;
+     *                   otherwise, the session is read from sessionstore.js
+     *
+     * @return the session string
+     */
+    public String readSessionFile(boolean readBackup) {
+        File sessionFile = getFile(readBackup ? "sessionstore.bak" : "sessionstore.js");
 
         try {
-            return readFile(sessionFile);
+            if (sessionFile != null && sessionFile.exists()) {
+                return readFile(sessionFile);
+            }
         } catch (IOException ioe) {
-            Log.i(LOGTAG, "Unable to read session file " + sessionFile.getAbsolutePath());
-            return null;
+            Log.e(LOGTAG, "Unable to read session file", ioe);
         }
+        return null;
     }
 
     public String readFile(String filename) throws IOException {
         File dir = getDir();
         if (dir == null) {
             throw new IOException("No profile directory found");
         }
         File target = new File(dir, filename);
--- a/mobile/android/base/SessionParser.java
+++ b/mobile/android/base/SessionParser.java
@@ -18,39 +18,53 @@ import org.json.JSONException;
 import org.json.JSONObject;
 
 public abstract class SessionParser {
     private static final String LOGTAG = "GeckoSessionParser";
 
     public class SessionTab {
         String mSelectedTitle;
         String mSelectedUrl;
+        boolean mIsSelected;
+        JSONObject mTabObject;
 
-        private SessionTab(String selectedTitle, String selectedUrl) {
+        private SessionTab(String selectedTitle, String selectedUrl, boolean isSelected, JSONObject tabObject) {
             mSelectedTitle = selectedTitle;
             mSelectedUrl = selectedUrl;
+            mIsSelected = isSelected;
+            mTabObject = tabObject;
         }
 
         public String getSelectedTitle() {
             return mSelectedTitle;
         }
 
         public String getSelectedUrl() {
             return mSelectedUrl;
         }
+
+        public boolean isSelected() {
+            return mIsSelected;
+        }
+
+        public JSONObject getTabObject() {
+            return mTabObject;
+        }
     };
 
     abstract public void onTabRead(SessionTab tab);
 
     public void parse(String sessionString) {
         final JSONArray tabs;
         final JSONObject window;
+        final int selected;
         try {
             window = new JSONObject(sessionString).getJSONArray("windows").getJSONObject(0);
             tabs = window.getJSONArray("tabs");
+            selected = window.optInt("selected", -1);
         } catch (JSONException e) {
             Log.e(LOGTAG, "JSON error", e);
             return;
         }
 
         for (int i = 0; i < tabs.length(); i++) {
             try {
                 JSONObject tab = tabs.getJSONObject(i);
@@ -58,16 +72,16 @@ public abstract class SessionParser {
                 JSONObject entry = tab.getJSONArray("entries").getJSONObject(index - 1);
                 String url = entry.getString("url");
 
                 String title = entry.optString("title");
                 if (title.length() == 0) {
                     title = url;
                 }
 
-                onTabRead(new SessionTab(title, url));
+                onTabRead(new SessionTab(title, url, (selected == i+1), tab));
             } catch (JSONException e) {
                 Log.e(LOGTAG, "error reading json file", e);
                 return;
             }
         }
     }
 }
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -35,16 +35,18 @@ public class Tabs implements GeckoEventL
     // Keeps track of how much has happened since we last updated our persistent tab store.
     private volatile int mScore = 0;
 
     public static final int LOADURL_NONE = 0;
     public static final int LOADURL_NEW_TAB = 1;
     public static final int LOADURL_USER_ENTERED = 2;
     public static final int LOADURL_PRIVATE = 4;
     public static final int LOADURL_PINNED = 8;
+    public static final int LOADURL_DELAY_LOAD = 16;
+    public static final int LOADURL_DESKTOP = 32;
 
     private static final int SCORE_INCREMENT_TAB_LOCATION_CHANGE = 5;
     private static final int SCORE_INCREMENT_TAB_SELECTED = 10;
     private static final int SCORE_THRESHOLD = 30;
 
     private static AtomicInteger sTabId = new AtomicInteger(0);
 
     private GeckoApp mActivity;
@@ -244,28 +246,28 @@ public class Tabs implements GeckoEventL
         try {
             if (event.startsWith("SessionHistory:")) {
                 Tab tab = getTab(message.getInt("tabID"));
                 if (tab != null) {
                     event = event.substring("SessionHistory:".length());
                     tab.handleSessionHistoryMessage(event, message);
                 }
             } else if (event.equals("Tab:Added")) {
+                String url = message.isNull("uri") ? null : message.getString("uri");
                 int id = message.getInt("tabID");
                 Tab tab = null;
 
                 if (mTabs.containsKey(id)) {
                     tab = mTabs.get(id);
+                    tab.updateURL(url);
                 } else {
-                    tab = addTab(id,
-                                 message.isNull("uri") ? null : message.getString("uri"),
-                                 message.getBoolean("external"),
-                                 message.getInt("parentId"),
-                                 message.getString("title"),
-                                 message.getBoolean("isPrivate"));
+                    tab = addTab(id, url, message.getBoolean("external"),
+                                          message.getInt("parentId"),
+                                          message.getString("title"),
+                                          message.getBoolean("isPrivate"));
                 }
 
                 if (message.getBoolean("selected"))
                     selectTab(tab.getId());
                 if (message.getBoolean("delayLoad"))
                     tab.setState(Tab.STATE_DELAYED);
                 if (message.getBoolean("desktopMode"))
                     tab.setDesktopMode(true);
@@ -462,50 +464,54 @@ public class Tabs implements GeckoEventL
      *                     to search for the url string; if null, the URL is loaded directly
      * @param parentId     ID of this tab's parent, or -1 if it has no parent
      * @param flags        flags used to load tab
      *
      * @return             the Tab if a new one was created; null otherwise
      */
     public Tab loadUrl(String url, String searchEngine, int parentId, int flags) {
         JSONObject args = new JSONObject();
-        int tabId = -1;
         Tab added = null;
+        boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0;
 
         try {
             boolean isPrivate = (flags & LOADURL_PRIVATE) != 0;
             boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0;
+            boolean desktopMode = (flags & LOADURL_DESKTOP) != 0;
 
             args.put("url", url);
             args.put("engine", searchEngine);
             args.put("parentId", parentId);
             args.put("userEntered", userEntered);
             args.put("newTab", (flags & LOADURL_NEW_TAB) != 0);
             args.put("isPrivate", isPrivate);
             args.put("pinned", (flags & LOADURL_PINNED) != 0);
+            args.put("delayLoad", delayLoad);
+            args.put("desktopMode", desktopMode);
 
             if ((flags & LOADURL_NEW_TAB) != 0) {
-                tabId = getNextTabId();
+                int tabId = getNextTabId();
                 args.put("tabID", tabId);
 
                 // The URL is updated for the tab once Gecko responds with the
                 // Tab:Added message. We can preliminarily set the tab's URL as
                 // long as it's a valid URI.
                 String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null;
 
                 added = addTab(tabId, tabUrl, false, parentId, url, isPrivate);
+                added.setDesktopMode(desktopMode);
             }
         } catch (Exception e) {
-            Log.w(LOGTAG, "Error building JSON arguments for loadUrl.");
+            Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
         }
 
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString()));
 
-        if (tabId != -1) {
-            selectTab(tabId);
+        if ((added != null) && !delayLoad) {
+            selectTab(added.getId());
         }
 
         return added;
     }
 
     /**
      * Open the url as a new tab, and mark the selected tab as its "parent".
      *
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -166,16 +166,17 @@ var BrowserApp = {
 
     Services.obs.addObserver(this, "Tab:Load", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
     Services.obs.addObserver(this, "Tab:Closed", false);
     Services.obs.addObserver(this, "Session:Back", false);
     Services.obs.addObserver(this, "Session:Forward", false);
     Services.obs.addObserver(this, "Session:Reload", false);
     Services.obs.addObserver(this, "Session:Stop", false);
+    Services.obs.addObserver(this, "Session:Restore", false);
     Services.obs.addObserver(this, "SaveAs:PDF", false);
     Services.obs.addObserver(this, "Browser:Quit", false);
     Services.obs.addObserver(this, "Preferences:Get", false);
     Services.obs.addObserver(this, "Preferences:Set", false);
     Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
     Services.obs.addObserver(this, "Sanitize:ClearData", false);
     Services.obs.addObserver(this, "FullScreen:Exit", false);
     Services.obs.addObserver(this, "Viewport:Change", false);
@@ -277,53 +278,18 @@ var BrowserApp = {
     event.initEvent("UIReady", true, false);
     window.dispatchEvent(event);
 
     // Restore the previous session
     // restoreMode = 0 means no restore
     // restoreMode = 1 means force restore (after an OOM kill)
     // restoreMode = 2 means restore only if we haven't crashed multiple times
     let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
-    if (restoreMode || ss.shouldRestore()) {
-      // A restored tab should not be active if we are loading a URL
-      let restoreToFront = false;
-
-      sendMessageToJava({
-        gecko: {
-          type: "Session:RestoreBegin"
-        }
-      });
-
-      // Make the restored tab active if we aren't loading an external URL
-      if (url == null) {
-        restoreToFront = true;
-      }
-
-      // Be ready to handle any restore failures by making sure we have a valid tab opened
-      let restoreCleanup = {
-        observe: function(aSubject, aTopic, aData) {
-          Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
-          if (aData == "fail") {
-            BrowserApp.addTab("about:home", {
-              showProgress: false,
-              selected: restoreToFront
-            });
-          }
-
-          sendMessageToJava({
-            gecko: {
-              type: "Session:RestoreEnd"
-            }
-          });
-        }
-      };
-      Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
-
-      // Start the restore
-      ss.restoreLastSession(restoreToFront, restoreMode == 1);
+    if (ss.shouldRestore()) {
+      this.restoreSession(false, null);
     }
 
     if (updated)
       this.onAppUpdated();
 
     // notify java that gecko has loaded
     sendMessageToJava({
       gecko: {
@@ -340,16 +306,49 @@ var BrowserApp = {
     });
 
 #ifdef MOZ_SAFE_BROWSING
     // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
     setTimeout(function() { SafeBrowsing.init(); }, 5000);
 #endif
   },
 
+  restoreSession: function (restoringOOM, sessionString) {
+    sendMessageToJava({
+      gecko: {
+        type: "Session:RestoreBegin"
+      }
+    });
+
+    // Be ready to handle any restore failures by making sure we have a valid tab opened
+    let restoreCleanup = {
+      observe: function (aSubject, aTopic, aData) {
+        Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
+
+        if (this.tabs.length == 0) {
+          this.addTab("about:home", {
+            showProgress: false,
+            selected: true
+          });
+        }
+
+        sendMessageToJava({
+          gecko: {
+            type: "Session:RestoreEnd"
+          }
+        });
+      }.bind(this)
+    };
+    Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
+
+    // Start the restore
+    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+    ss.restoreLastSession(restoringOOM, sessionString);
+  },
+
   isAppUpdated: function() {
     let savedmstone = null;
     try {
       savedmstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
     } catch (e) {
     }
 #expand    let ourmstone = "__MOZ_APP_VERSION__";
     if (ourmstone != savedmstone) {
@@ -1068,23 +1067,26 @@ var BrowserApp = {
       let data = JSON.parse(aData);
 
       // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
       // inheriting the currently loaded document's principal.
       let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
       if (data.userEntered)
         flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER;
 
+      let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
       let params = {
-        selected: true,
+        selected: !delayLoad,
         parentId: ("parentId" in data) ? data.parentId : -1,
         flags: flags,
         tabID: data.tabID,
-        isPrivate: data.isPrivate,
-        pinned: data.pinned
+        isPrivate: (data.isPrivate == true),
+        pinned: (data.pinned == true),
+        delayLoad: (delayLoad == true),
+        desktopMode: (data.desktopMode == true)
       };
 
       let url = data.url;
       if (data.engine) {
         let engine = Services.search.getEngineByName(data.engine);
         if (engine) {
           let submission = engine.getSubmission(url);
           url = submission.uri.spec;
@@ -1141,16 +1143,19 @@ var BrowserApp = {
     } else if (aTopic == "ToggleProfiling") {
       let profiler = Cc["@mozilla.org/tools/profiler;1"].
                        getService(Ci.nsIProfiler);
       if (profiler.IsActive()) {
         profiler.StopProfiler();
       } else {
         profiler.StartProfiler(100000, 25, ["stackwalk"], 1);
       }
+    } else if (aTopic == "Session:Restore") {
+      let data = JSON.parse(aData);
+      this.restoreSession(data.restoringOOM, data.sessionString);
     }
   },
 
   get defaultBrowserWidth() {
     delete this.defaultBrowserWidth;
     let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
     return this.defaultBrowserWidth = width;
   },
@@ -2336,16 +2341,17 @@ Tab.prototype = {
 
     this.browser.stop();
 
     let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
     frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
 
     // only set tab uri if uri is valid
     let uri = null;
+    let title = aParams.title || aURL;
     try {
       uri = Services.io.newURI(aURL, null, null).spec;
     } catch (e) {}
 
     if (!aParams.zombifying) {
       if ("tabID" in aParams) {
         this.id = aParams.tabID;
       } else {
@@ -2361,17 +2367,17 @@ Tab.prototype = {
       let message = {
         gecko: {
           type: "Tab:Added",
           tabID: this.id,
           uri: uri,
           parentId: ("parentId" in aParams) ? aParams.parentId : -1,
           external: ("external" in aParams) ? aParams.external : false,
           selected: ("selected" in aParams) ? aParams.selected : true,
-          title: aParams.title || aURL,
+          title: title,
           delayLoad: aParams.delayLoad || false,
           desktopMode: this.desktopMode,
           isPrivate: isPrivate
         }
       };
       sendMessageToJava(message);
 
       this.overscrollController = new OverscrollController(this);
@@ -2395,17 +2401,28 @@ Tab.prototype = {
     this.browser.addEventListener("PluginClickToPlay", this, true);
     this.browser.addEventListener("PluginPlayPreview", this, true);
     this.browser.addEventListener("PluginNotFound", this, true);
     this.browser.addEventListener("pageshow", this, true);
 
     Services.obs.addObserver(this, "before-first-paint", false);
     Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
 
-    if (!aParams.delayLoad) {
+    if (aParams.delayLoad) {
+      // If this is a zombie tab, attach restore data so the tab will be
+      // restored when selected
+      this.browser.__SS_data = {
+        entries: [{
+          url: aURL,
+          title: title
+        }],
+        index: 1
+      };
+      this.browser.__SS_restore = true;
+    } else {
       let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
       let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
       let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
       let charset = "charset" in aParams ? aParams.charset : null;
 
       // This determines whether or not we show the progress throbber in the urlbar
       this.showProgress = "showProgress" in aParams ? aParams.showProgress : true;
 
--- a/mobile/android/components/SessionStore.idl
+++ b/mobile/android/components/SessionStore.idl
@@ -9,17 +9,17 @@ interface nsIDOMNode;
 
 /**
  * nsISessionStore keeps track of the current browsing state.
  *
  * The nsISessionStore API operates mostly on browser windows and the browser
  * tabs contained in them.
  */
 
-[scriptable, uuid(766a09c1-d21b-4bf8-9fe3-8b34b716251a)]
+[scriptable, uuid(15152edf-6c99-4277-9020-076be4653c69)]
 interface nsISessionStore : nsISupports
 {
   /**
    * Get the current browsing state.
    * @returns a JSON string representing the session state.
    */
   AString getBrowserState();
 
@@ -72,13 +72,14 @@ interface nsISessionStore : nsISupports
 
   /**
    * @returns A boolean indicating we should restore previous browser session
    */
   boolean shouldRestore();
 
   /**
    * Restores the previous browser session using a fast, lightweight strategy
-   * @param aBringToFront should a restored tab be brought to the foreground?
-   * @param aForceRestore  whether we need to force a restore, regardless of the recent crash situation
+   * @param aRestoringOOM  Whether this is an OOM restore from Android
+   * @param aSessionString The session string to restore from. If null, the
+   *                       backup session file is read from.
    */
-  void restoreLastSession(in boolean aBringToFront, in boolean aForceRestore);
+  void restoreLastSession(in boolean aRestoringOOM, in AString aSessionString);
 };
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -39,62 +39,43 @@ SessionStore.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
                                          Ci.nsIDOMEventListener,
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   _windows: {},
   _lastSaveTime: 0,
-  _lastSessionTime: 0,
   _interval: 10000,
   _maxTabsUndo: 1,
   _shouldRestore: false,
 
   init: function ss_init() {
     // Get file references
     this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
     this._sessionFileBackup = this._sessionFile.clone();
     this._sessionCache = this._sessionFile.clone();
     this._sessionFile.append("sessionstore.js");
     this._sessionFileBackup.append("sessionstore.bak");
     this._sessionCache.append("sessionstoreCache");
 
     this._loadState = STATE_STOPPED;
 
     try {
-      if (this._sessionFile.exists()) {
-        // We move sessionstore.js -> sessionstore.bak on quit, so the
-        // existence of sessionstore.js indicates a crash
-        this._lastSessionTime = this._sessionFile.lastModifiedTime;
-        let delta = Date.now() - this._lastSessionTime;
-        let timeout = Services.prefs.getIntPref("browser.sessionstore.resume_from_crash_timeout");
-
-        // Disable crash recovery if we have exceeded the timeout
-        this._shouldRestore = (delta <= (timeout * 60000));
-        if (!this._shouldRestore) {
-          this._sessionFile.clone().moveTo(null, this._sessionFileBackup.leafName);
-        }
-      }
-
       if (!this._sessionCache.exists() || !this._sessionCache.isDirectory())
         this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
     } catch (ex) {
       Cu.reportError(ex); // file was write-locked?
     }
 
     this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
     this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
 
-    // Disable crash recovery if it has been turned off
-    if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"))
-      this._shouldRestore = false;
-
     // Do we need to restore session just this once, in case of a restart?
-    if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
+    if (this._sessionFileBackup.exists() && Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
       Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false);
       this._shouldRestore = true;
     }
   },
 
   _clearDisk: function ss_clearDisk() {
     if (this._sessionFile.exists()) {
       try {
@@ -308,22 +289,16 @@ SessionStore.prototype = {
     // Assign it a unique identifier (timestamp) and create its data object
     aWindow.__SSID = "window" + Date.now();
     this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] };
 
     // Perform additional initialization when the first window is loading
     if (this._loadState == STATE_STOPPED) {
       this._loadState = STATE_RUNNING;
       this._lastSaveTime = Date.now();
-
-      // Nothing to restore, notify observers things are complete
-      if (!this._shouldRestore) {
-        this._clearCache();
-        Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
-      }
     }
 
     // Add tab change listeners to all already existing tabs
     let tabs = aWindow.BrowserApp.tabs;
     for (let i = 0; i < tabs.length; i++)
       this.onTabAdd(aWindow, tabs[i].browser, true);
 
     // Notification of tab add/remove/selection
@@ -832,46 +807,66 @@ SessionStore.prototype = {
     aHistory.getEntryAtIndex(activeIndex, true);
     aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
   },
 
   getBrowserState: function ss_getBrowserState() {
     return this._getCurrentState();
   },
 
-  _restoreWindow: function ss_restoreWindow(aState, aBringToFront) {
+  _restoreWindow: function ss_restoreWindow(aData) {
+    let state;
+    try {
+      state = JSON.parse(aData);
+    } catch (e) {
+      Cu.reportError("SessionStore: invalid session JSON");
+      return false;
+    }
+
     // To do a restore, we must have at least one window with one tab
-    if (!aState || aState.windows.length == 0 || !aState.windows[0].tabs || aState.windows[0].tabs.length == 0) {
+    if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) {
+      Cu.reportError("SessionStore: no tabs to restore");
       return false;
     }
 
     let window = Services.wm.getMostRecentWindow("navigator:browser");
 
-    let tabs = aState.windows[0].tabs;
-    let selected = aState.windows[0].selected;
+    let tabs = state.windows[0].tabs;
+    let selected = state.windows[0].selected;
     if (selected == null || selected > tabs.length) // Clamp the selected index if it's bogus
       selected = 1;
 
     for (let i = 0; i < tabs.length; i++) {
       let tabData = tabs[i];
-      let isSelected = (i + 1 == selected) && aBringToFront;
       let entry = tabData.entries[tabData.index - 1];
 
-      // Add a tab, but don't load the URL until we need to
-      let params = {
-        selected: isSelected,
-        delayLoad: true,
-        title: entry.title,
-        desktopMode: tabData.desktopMode == true,
-        isPrivate: tabData.isPrivate == true
-      };
-      let tab = window.BrowserApp.addTab(entry.url, params);
+      // Use stubbed tab if we've already created it; otherwise, make a new tab
+      let tab;
+      if (tabData.tabId == null) {
+        let params = {
+          selected: (selected == i+1),
+          delayLoad: true,
+          title: entry.title,
+          desktopMode: (tabData.desktopMode == true),
+          isPrivate: (tabData.isPrivate == true)
+        };
+        tab = window.BrowserApp.addTab(entry.url, params);
+      } else {
+        tab = window.BrowserApp.getTabForId(tabData.tabId);
+        delete tabData.tabId;
 
-      if (isSelected) {
+        // Don't restore tab if user has closed it
+        if (tab == null) {
+          continue;
+        }
+      }
+
+      if (window.BrowserApp.selectedTab == tab) {
         this._restoreHistory(tabData, tab.browser.sessionHistory);
+        delete tab.browser.__SS_restore;
       } else {
         // Make sure the browser has its session data for the delay reload
         tab.browser.__SS_data = tabData;
         tab.browser.__SS_restore = true;
       }
 
       tab.browser.__SS_extdata = tabData.extData;
     }
@@ -977,79 +972,85 @@ SessionStore.prototype = {
     else
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
   },
 
   shouldRestore: function ss_shouldRestore() {
     return this._shouldRestore;
   },
 
-  restoreLastSession: function ss_restoreLastSession(aBringToFront, aForceRestore) {
+  restoreLastSession: function ss_restoreLastSession(aRestoringOOM, aSessionString) {
     let self = this;
+
+    function restoreWindow(data) {
+      if (!self._restoreWindow(data)) {
+        throw "Could not restore window";
+      }
+
+      notifyObservers();
+    }
+
     function notifyObservers(aMessage) {
       self._clearCache();
       Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || "");
     }
 
-    let sessionFile = this._sessionFile;
+    try {
+      if (!aRestoringOOM && !this._shouldRestore) {
+        // If we're here, it means we're restoring from a crash (not an OOM
+        // kill). Check prefs and other conditions to make sure we want to
+        // continue with the restore.
+
+        // Disable crash recovery if it has been turned off.
+        if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash")) {
+          throw "Restore is disabled via prefs";
+        }
 
-    // aForceRestore will be true when we are recovering from Android OOM kills
-    if (!aForceRestore) {
-      // If we are not recovering from an OOM kill (i.e., we actually crashed),
-      // move sessionstore.js -> sessionstore.bak. sessionstore.bak is used in
-      // about:home to read the "tabs from last time", so since we've started a
-      // new session after the crash, we need to make sure sessionstore.bak is
-      // current. We do not move sessionstore.js -> sessionstore.bak if we had
-      // an OOM kill since restoring from an OOM kill should look like the same
-      // session as before (so the "tabs from last time" should stay the same).
-      if (sessionFile.exists()) {
-        sessionFile.clone().moveTo(null, this._sessionFileBackup.leafName);
-        sessionFile = this._sessionFileBackup;
+        // Check to see if we've exceeded the maximum number of crashes to
+        // avoid a crash loop
+        let maxCrashes = Services.prefs.getIntPref("browser.sessionstore.max_resumed_crashes");
+        let recentCrashes = Services.prefs.getIntPref("browser.sessionstore.recent_crashes") + 1;
+        Services.prefs.setIntPref("browser.sessionstore.recent_crashes", recentCrashes);
+        Services.prefs.savePrefFile(null);
+
+        if (recentCrashes > maxCrashes) {
+          throw "Exceeded maximum number of allowed restores";
+        }
       }
 
-      let maxCrashes = Services.prefs.getIntPref("browser.sessionstore.max_resumed_crashes");
-      let recentCrashes = Services.prefs.getIntPref("browser.sessionstore.recent_crashes") + 1;
-      Services.prefs.setIntPref("browser.sessionstore.recent_crashes", recentCrashes);
-      Services.prefs.savePrefFile(null);
-
-      if (recentCrashes > maxCrashes) {
-        notifyObservers("fail");
-        return;
-      }
-    }
-
-    if (!sessionFile.exists()) {
-      Cu.reportError("SessionStore: session file does not exist");
-      notifyObservers("fail");
-      return;
-    }
+      // Normally, we'll receive the session string from Java, but there are
+      // cases where we may want to restore that Java cannot detect (e.g., if
+      // browser.sessionstore.resume_session_once is true). In these cases, the
+      // session will be read from sessionstore.bak (which is also used for
+      // "tabs from last time").
+      if (aSessionString == null) {
+        if (!this._sessionFileBackup.exists()) {
+          throw "Session file doesn't exist";
+        }
 
-    try {
-      let channel = NetUtil.newChannel(sessionFile);
-      channel.contentType = "application/json";
-      NetUtil.asyncFetch(channel, function(aStream, aResult) {
-        try {
-          if (!Components.isSuccessCode(aResult)) {
-            throw new Error("Could not fetch session file");
-          }
-
-          let data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || "";
-          aStream.close();
+        let channel = NetUtil.newChannel(this._sessionFileBackup);
+        channel.contentType = "application/json";
+        NetUtil.asyncFetch(channel, function(aStream, aResult) {
+          try {
+            if (!Components.isSuccessCode(aResult)) {
+              throw "Could not fetch session file";
+            }
 
-          let state = JSON.parse(data);
-          if (self._restoreWindow(state, aBringToFront)) {
-            notifyObservers();
-          } else {
-            throw new Error("Could not restore window");
+            let data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || "";
+            aStream.close();
+            
+            restoreWindow(data);
+          } catch (e) {
+            Cu.reportError("SessionStore: " + e.message);
+            notifyObservers("fail");
           }
-        } catch (e) {
-          Cu.reportError("SessionStore: " + e.message);
-          notifyObservers("fail");
-        }
-      });
+        });
+      } else {
+        restoreWindow(aSessionString);
+      }
     } catch (e) {
-      Cu.reportError("SessionStore: Could not create session file channel");
+      Cu.reportError("SessionStore: " + e);
       notifyObservers("fail");
     }
   }
 };
 
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);