Bug 677310 - Thumbnails are lost when switching to private browsing mode; r=dietrich
authorTim Taubert <tim.taubert@gmx.de>
Thu, 01 Sep 2011 15:17:27 +0200
changeset 76390 b5bb731a58a9a823f6a0f458343c5f454a0afbae
parent 76389 63becbe85737890d364f39cf65188cbceca3c93b
child 76391 b7a7127396f63ac9400ae382efe87314db64339c
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
reviewersdietrich
bugs677310
milestone9.0a1
Bug 677310 - Thumbnails are lost when switching to private browsing mode; r=dietrich
browser/base/content/tabview/storage.js
browser/base/content/tabview/tabitems.js
browser/base/content/tabview/thumbnailStorage.js
browser/base/content/tabview/ui.js
browser/base/content/test/tabview/Makefile.in
browser/base/content/test/tabview/browser_tabview_bug597248.js
browser/base/content/test/tabview/browser_tabview_bug604699.js
browser/base/content/test/tabview/browser_tabview_bug627288.js
browser/base/content/test/tabview/browser_tabview_bug677310.js
browser/base/content/test/tabview/browser_tabview_storage_policy.js
browser/base/content/test/tabview/browser_tabview_thumbnail_storage.js
--- a/browser/base/content/tabview/storage.js
+++ b/browser/base/content/tabview/storage.js
@@ -94,57 +94,37 @@ let Storage = {
   },
 
   // ----------
   // Function: saveTab
   // Saves the data for a single tab.
   saveTab: function Storage_saveTab(tab, data) {
     Utils.assert(tab, "tab");
 
-    if (data != null) {
-      let imageData = data.imageData;
-      // Remove imageData from payload
-      delete data.imageData;
-
-      if (imageData != null)
-        ThumbnailStorage.saveThumbnail(tab, imageData);
-    }
-
     this._sessionStore.setTabValue(tab, this.TAB_DATA_IDENTIFIER,
       JSON.stringify(data));
   },
 
   // ----------
   // Function: getTabData
-  // Load tab data from session store and return it. Asynchrously loads the tab's
-  // thumbnail from the cache and calls <callback>(imageData) when done.
-  getTabData: function Storage_getTabData(tab, callback) {
+  // Load tab data from session store and return it.
+  getTabData: function Storage_getTabData(tab) {
     Utils.assert(tab, "tab");
-    Utils.assert(typeof callback == "function", "callback arg must be a function");
 
     let existingData = null;
 
     try {
       let tabData = this._sessionStore.getTabValue(tab, this.TAB_DATA_IDENTIFIER);
-      if (tabData != "") {
+      if (tabData != "")
         existingData = JSON.parse(tabData);
-      }
     } catch (e) {
       // getTabValue will fail if the property doesn't exist.
       Utils.log(e);
     }
 
-    if (existingData) {
-      ThumbnailStorage.loadThumbnail(
-        tab, existingData.url,
-        function(status, imageData) { 
-          callback(imageData);
-        }
-      );
-    }
     return existingData;
   },
 
   // ----------
   // Function: saveGroupItem
   // Saves the data for a single groupItem, associated with a specific window.
   saveGroupItem: function Storage_saveGroupItem(win, data) {
     var id = data.id;
--- a/browser/base/content/tabview/tabitems.js
+++ b/browser/base/content/tabview/tabitems.js
@@ -64,40 +64,45 @@ function TabItem(tab, options) {
   document.body.appendChild(TabItems.fragment().cloneNode(true));
   
   // The document fragment contains just one Node
   // As per DOM3 appendChild: it will then be the last child
   let div = document.body.lastChild;
   let $div = iQ(div);
 
   this._cachedImageData = null;
+  this._thumbnailNeedsSaving = false;
   this.canvasSizeForced = false;
   this.$thumb = iQ('.thumb', $div);
   this.$fav   = iQ('.favicon', $div);
   this.$tabTitle = iQ('.tab-title', $div);
   this.$canvas = iQ('.thumb canvas', $div);
   this.$cachedThumb = iQ('img.cached-thumb', $div);
   this.$favImage = iQ('.favicon>img', $div);
   this.$close = iQ('.close', $div);
 
   this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]);
 
+  let self = this;
+
+  // when we paint onto the canvas make sure our thumbnail gets saved
+  this.tabCanvas.addSubscriber("painted", function () {
+    self._thumbnailNeedsSaving = true;
+  });
+
   this.defaultSize = new Point(TabItems.tabWidth, TabItems.tabHeight);
   this._hidden = false;
   this.isATabItem = true;
   this.keepProportional = true;
   this._hasBeenDrawn = false;
   this._reconnected = false;
+  this.isDragging = false;
   this.isStacked = false;
   this.url = "";
 
-  var self = this;
-
-  this.isDragging = false;
-
   // Read off the total vertical and horizontal padding on the tab container
   // and cache this value, as it must be the same for every TabItem.
   if (Utils.isEmptyObject(TabItems.tabItemPadding)) {
     TabItems.tabItemPadding.x = parseInt($div.css('padding-left'))
         + parseInt($div.css('padding-right'));
   
     TabItems.tabItemPadding.y = parseInt($div.css('padding-top'))
         + parseInt($div.css('padding-bottom'));
@@ -189,107 +194,162 @@ TabItem.prototype = Utils.extend(new Ite
 
   // ----------
   // Function: showCachedData
   // Shows the cached data i.e. image and title.  Note: this method should only
   // be called at browser startup with the cached data avaliable.
   //
   // Parameters:
   //   tabData - the tab data
-  showCachedData: function TabItem_showCachedData(tabData) {
-    this._cachedImageData = tabData.imageData;
+  //   imageData - the image data
+  showCachedData: function TabItem_showCachedData(tabData, imageData) {
+    this._cachedImageData = imageData;
     this.$cachedThumb.attr("src", this._cachedImageData).show();
-    this.$canvas.css({opacity: 0.0});
+    this.$canvas.css({opacity: 0});
     this.$tabTitle.text(tabData.title ? tabData.title : "");
+
+    this._sendToSubscribers("showingCachedData");
   },
 
   // ----------
   // Function: hideCachedData
   // Hides the cached data i.e. image and title and show the canvas.
   hideCachedData: function TabItem_hideCachedData() {
     this.$cachedThumb.hide();
     this.$canvas.css({opacity: 1.0});
     if (this._cachedImageData)
       this._cachedImageData = null;
   },
 
   // ----------
   // Function: getStorageData
   // Get data to be used for persistent storage of this object.
-  //
-  // Parameters:
-  //   getImageData - true to include thumbnail pixels (and page title as well); default false
-  getStorageData: function TabItem_getStorageData(getImageData) {
-    let imageData = null;
-
-    if (getImageData) { 
-      if (this._cachedImageData)
-        imageData = this._cachedImageData;
-      else if (this.tabCanvas)
-        imageData = this.tabCanvas.toImageData();
-    }
-
+  getStorageData: function TabItem_getStorageData() {
     return {
       url: this.tab.linkedBrowser.currentURI.spec,
       groupID: (this.parent ? this.parent.id : 0),
-      imageData: imageData,
-      title: getImageData && this.tab.label || null
+      title: this.tab.label
     };
   },
 
   // ----------
   // Function: save
   // Store persistent for this object.
-  //
-  // Parameters:
-  //   saveImageData - true to include thumbnail pixels (and page title as well); default false
-  save: function TabItem_save(saveImageData) {
-    try{
+  save: function TabItem_save() {
+    try {
       if (!this.tab || this.tab.parentNode == null || !this._reconnected) // too soon/late to save
         return;
 
-      var data = this.getStorageData(saveImageData);
+      let data = this.getStorageData();
       if (TabItems.storageSanity(data))
         Storage.saveTab(this.tab, data);
     } catch(e) {
       Utils.log("Error in saving tab value: "+e);
     }
   },
 
   // ----------
+  // Function: loadThumbnail
+  // Loads the tabItems thumbnail.
+  loadThumbnail: function TabItem_loadThumbnail(tabData) {
+    Utils.assert(tabData, "invalid or missing argument <tabData>");
+
+    let self = this;
+
+    function TabItem_loadThumbnail_callback(error, imageData) {
+      // we could have been unlinked while waiting for the thumbnail to load
+      if (error || !imageData || !self.tab)
+        return;
+
+      self._sendToSubscribers("loadedCachedImageData");
+
+      // If we have a cached image, then show it if the loaded URL matches
+      // what the cache is from, OR the loaded URL is blank, which means
+      // that the page hasn't loaded yet.
+      let currentUrl = self.tab.linkedBrowser.currentURI.spec;
+      if (tabData.url == currentUrl || currentUrl == "about:blank")
+        self.showCachedData(tabData, imageData);
+    }
+
+    ThumbnailStorage.loadThumbnail(tabData.url, TabItem_loadThumbnail_callback);
+  },
+
+  // ----------
+  // Function: saveThumbnail
+  // Saves the tabItems thumbnail.
+  saveThumbnail: function TabItem_saveThumbnail(options) {
+    if (!this.tabCanvas)
+      return;
+
+    // nothing to do if the thumbnail hasn't changed
+    if (!this._thumbnailNeedsSaving)
+      return;
+
+    // check the storage policy to see if we're allowed to store the thumbnail
+    if (!StoragePolicy.canStoreThumbnailForTab(this.tab)) {
+      this._sendToSubscribers("deniedToSaveImageData");
+      return;
+    }
+
+    let url = this.tab.linkedBrowser.currentURI.spec;
+    let delayed = this._saveThumbnailDelayed;
+    let synchronously = (options && options.synchronously);
+
+    // is there a delayed save waiting?
+    if (delayed) {
+      // check if url has changed since last call to saveThumbnail
+      if (!synchronously && url == delayed.url)
+        return;
+
+      // url has changed in the meantime, clear the timeout
+      clearTimeout(delayed.timeout);
+    }
+
+    let self = this;
+
+    function callback(error) {
+      if (!error) {
+        self._thumbnailNeedsSaving = false;
+        self._sendToSubscribers("savedCachedImageData");
+      }
+    }
+
+    function doSaveThumbnail() {
+      self._saveThumbnailDelayed = null;
+
+      // we could have been unlinked in the meantime
+      if (!self.tabCanvas)
+        return;
+
+      let imageData = self.tabCanvas.toImageData();
+      ThumbnailStorage.saveThumbnail(url, imageData, callback, options);
+    }
+
+    if (synchronously) {
+      doSaveThumbnail();
+    } else {
+      let timeout = setTimeout(doSaveThumbnail, 2000);
+      this._saveThumbnailDelayed = {url: url, timeout: timeout};
+    }
+  },
+
+  // ----------
   // Function: _reconnect
   // Load the reciever's persistent data from storage. If there is none, 
   // treats it as a new tab. 
   _reconnect: function TabItem__reconnect() {
     Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected");
     Utils.assertThrow(this.tab, "should have a xul:tab");
 
-    let tabData = null;
     let self = this;
-    let imageDataCb = function(imageData) {
-      // we could have been unlinked while waiting for the thumbnail to load
-      if (!self.tab)
-        return;
-
-      Utils.assertThrow(tabData, "tabData");
-      tabData.imageData = imageData;
+    let tabData = Storage.getTabData(this.tab);
 
-      let currentUrl = self.tab.linkedBrowser.currentURI.spec;
-      // If we have a cached image, then show it if the loaded URL matches
-      // what the cache is from, OR the loaded URL is blank, which means
-      // that the page hasn't loaded yet.
-      if (tabData.imageData &&
-          (tabData.url == currentUrl || currentUrl == 'about:blank')) {
-        self.showCachedData(tabData);
-      }
-    };
-    // getTabData returns the sessionstore contents, but passes
-    // a callback to run when the thumbnail is finally loaded.
-    tabData = Storage.getTabData(this.tab, imageDataCb);
     if (tabData && TabItems.storageSanity(tabData)) {
+      this.loadThumbnail(tabData);
+
       if (self.parent)
         self.parent.remove(self, {immediately: true});
 
       let groupItem;
 
       if (tabData.groupID) {
         groupItem = GroupItems.groupItem(tabData.groupID);
       } else {
@@ -931,16 +991,17 @@ let TabItems = {
           tabItem.$canvas[0].height = h;
         }
       }
 
       this._lastUpdateTime = Date.now();
       tabItem._lastTabUpdateTime = this._lastUpdateTime;
 
       tabItem.tabCanvas.paint();
+      tabItem.saveThumbnail();
 
       // ___ cache
       if (tabItem.isShowingCachedData())
         tabItem.hideCachedData();
 
       // ___ notify subscribers that a full update has completed.
       tabItem._sendToSubscribers("updated");
     } catch(e) {
@@ -1141,23 +1202,32 @@ let TabItems = {
   // Returns a copy of the master array of <TabItem>s.
   getItems: function TabItems_getItems() {
     return Utils.copy(this.items);
   },
 
   // ----------
   // Function: saveAll
   // Saves all open <TabItem>s.
-  //
-  // Parameters:
-  //   saveImageData - true to include thumbnail pixels (and page title as well); default false
-  saveAll: function TabItems_saveAll(saveImageData) {
-    var items = this.getItems();
-    items.forEach(function(item) {
-      item.save(saveImageData);
+  saveAll: function TabItems_saveAll() {
+    let tabItems = this.getItems();
+
+    tabItems.forEach(function TabItems_saveAll_forEach(tabItem) {
+      tabItem.save();
+    });
+  },
+
+  // ----------
+  // Function: saveAllThumbnails
+  // Saves thumbnails of all open <TabItem>s.
+  saveAllThumbnails: function TabItems_saveAllThumbnails(options) {
+    let tabItems = this.getItems();
+
+    tabItems.forEach(function TabItems_saveAllThumbnails_forEach(tabItem) {
+      tabItem.saveThumbnail(options);
     });
   },
 
   // ----------
   // Function: storageSanity
   // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage)
   // and returns true if it looks valid.
   // TODO: this is a stub, please implement
@@ -1337,17 +1407,17 @@ TabPriorityQueue.prototype = {
 // Class: TabCanvas
 // Takes care of the actual canvas for the tab thumbnail
 // Does not need to be accessed from outside of tabitems.js
 function TabCanvas(tab, canvas) {
   this.tab = tab;
   this.canvas = canvas;
 };
 
-TabCanvas.prototype = {
+TabCanvas.prototype = Utils.extend(new Subscribable(), {
   // ----------
   // Function: toString
   // Prints [TabCanvas (tab)] for debug use
   toString: function TabCanvas_toString() {
     return "[TabCanvas (" + this.tab + ")]";
   },
 
   // ----------
@@ -1381,16 +1451,18 @@ TabCanvas.prototype = {
       } catch (e) {
         Utils.error('paint', e);
       }
     } else {
       // General case where nearest neighbor algorithm looks good
       // Draw directly to the destination canvas
       this._drawWindow(ctx, w, h, bgColor);
     }
+
+    this._sendToSubscribers("painted");
   },
 
   // ----------
   // Function: _fillCanvasBackground
   // Draws a rectangle of <width>x<height> with color <bgColor> to the given
   // canvas context.
   _fillCanvasBackground: function TabCanvas__fillCanvasBackground(ctx, width, height, bgColor) {
     ctx.fillStyle = bgColor;
@@ -1449,9 +1521,9 @@ TabCanvas.prototype = {
     return new Rect(left, top, width, height);
   },
 
   // ----------
   // Function: toImageData
   toImageData: function TabCanvas_toImageData() {
     return this.canvas.toDataURL("image/png");
   }
-};
+});
--- a/browser/base/content/tabview/thumbnailStorage.js
+++ b/browser/base/content/tabview/thumbnailStorage.js
@@ -79,127 +79,125 @@ let ThumbnailStorage = {
       "init");
   },
 
   // ----------
   // Function: _openCacheEntry
   // Opens a cache entry for the given <url> and requests access <access>.
   // Calls <successCallback>(entry) when the entry was successfully opened with
   // requested access rights. Otherwise calls <errorCallback>().
-  _openCacheEntry: function ThumbnailStorage__openCacheEntry(url, access, successCallback, errorCallback) {
-    let onCacheEntryAvailable = function(entry, accessGranted, status) {
+  //
+  // Parameters:
+  //   url - the url to use as the storage key
+  //   access - access flags, see Ci.nsICache.ACCESS_*
+  //   successCallback - the callback to be called on success
+  //   errorCallback - the callback to be called when an error occured
+  //   options - an object with additional parameters, see below
+  //
+  // Possible options:
+  //   synchronously - set to true to force sync mode
+  _openCacheEntry:
+    function ThumbnailStorage__openCacheEntry(url, access, successCallback,
+                                              errorCallback, options) {
+    Utils.assert(url, "invalid or missing argument <url>");
+    Utils.assert(access, "invalid or missing argument <access>");
+    Utils.assert(successCallback, "invalid or missing argument <successCallback>");
+    Utils.assert(errorCallback, "invalid or missing argument <errorCallback>");
+
+    function onCacheEntryAvailable(entry, accessGranted, status) {
       if (entry && access == accessGranted && Components.isSuccessCode(status)) {
         successCallback(entry);
       } else {
-        entry && entry.close();
+        if (entry)
+          entry.close();
+
         errorCallback();
       }
     }
 
     let key = this.CACHE_PREFIX + url;
 
-    // switch to synchronous mode if parent window is about to close
-    if (UI.isDOMWindowClosing) {
+    if (options && options.synchronously) {
       let entry = this._cacheSession.openCacheEntry(key, access, true);
       let status = Cr.NS_OK;
       onCacheEntryAvailable(entry, entry.accessGranted, status);
     } else {
       let listener = new CacheListener(onCacheEntryAvailable);
       this._cacheSession.asyncOpenCacheEntry(key, access, listener);
     }
   },
 
   // ----------
   // Function: saveThumbnail
-  // Saves the <imageData> to the cache using the given <url> as key.
-  // Calls <callback>(status, data) when finished, passing true or false
-  // (indicating whether the operation succeeded).
-  saveThumbnail: function ThumbnailStorage_saveThumbnail(tab, imageData, callback) {
-    Utils.assert(tab, "tab");
-    Utils.assert(imageData, "imageData");
+  // Saves the given thumbnail in the cache.
+  //
+  // Parameters:
+  //   url - the url to use as the storage key
+  //   imageData - the image data to save for the given key
+  //   callback - the callback that is called when the operation is finished
+  //   options - an object with additional parameters, see below
+  //
+  // Possible options:
+  //   synchronously - set to true to force sync mode
+  saveThumbnail:
+    function ThumbnailStorage_saveThumbnail(url, imageData, callback, options) {
+    Utils.assert(url, "invalid or missing argument <url>");
+    Utils.assert(imageData, "invalid or missing argument <imageData>");
+    Utils.assert(callback, "invalid or missing argument <callback>");
 
-    if (!StoragePolicy.canStoreThumbnailForTab(tab)) {
-      tab._tabViewTabItem._sendToSubscribers("deniedToCacheImageData");
-      if (callback)
-        callback(false);
-      return;
-    }
-
+    let synchronously = (options && options.synchronously);
     let self = this;
 
-    let completed = function(status) {
-      if (callback)
-        callback(status);
-
-      if (status) {
-        // Notify subscribers
-        tab._tabViewTabItem._sendToSubscribers("savedCachedImageData");
-      } else {
-        Utils.log("Error while saving thumbnail: " + e);
-      }
-    };
-
-    let onCacheEntryAvailable = function(entry) {
+    function onCacheEntryAvailable(entry) {
       let outputStream = entry.openOutputStream(0);
 
-      let cleanup = function() {
+      function cleanup() {
         outputStream.close();
         entry.close();
       }
 
-      // switch to synchronous mode if parent window is about to close
-      if (UI.isDOMWindowClosing) {
+      // synchronous mode
+      if (synchronously) {
         outputStream.write(imageData, imageData.length);
         cleanup();
-        completed(true);
+        callback();
         return;
       }
 
       // asynchronous mode
       let inputStream = new self._stringInputStream(imageData, imageData.length);
       gNetUtil.asyncCopy(inputStream, outputStream, function (result) {
         cleanup();
         inputStream.close();
-        completed(Components.isSuccessCode(result));
+        callback(Components.isSuccessCode(result) ? "" : "failure");
       });
     }
 
-    let onCacheEntryUnavailable = function() {
-      completed(false);
+    function onCacheEntryUnavailable() {
+      callback("unavailable");
     }
 
-    this._openCacheEntry(tab.linkedBrowser.currentURI.spec, 
-        Ci.nsICache.ACCESS_WRITE, onCacheEntryAvailable, 
-        onCacheEntryUnavailable);
+    this._openCacheEntry(url, Ci.nsICache.ACCESS_WRITE, onCacheEntryAvailable,
+                         onCacheEntryUnavailable, options);
   },
 
   // ----------
   // Function: loadThumbnail
-  // Asynchrously loads image data from the cache using the given <url> as key.
-  // Calls <callback>(status, data) when finished, passing true or false
-  // (indicating whether the operation succeeded) and the retrieved image data.
-  loadThumbnail: function ThumbnailStorage_loadThumbnail(tab, url, callback) {
-    Utils.assert(tab, "tab");
-    Utils.assert(url, "url");
-    Utils.assert(typeof callback == "function", "callback arg must be a function");
+  // Loads a thumbnail from the cache.
+  //
+  // Parameters:
+  //   url - the url to use as the storage key
+  //   callback - the callback that is called when the operation is finished
+  loadThumbnail: function ThumbnailStorage_loadThumbnail(url, callback) {
+    Utils.assert(url, "invalid or missing argument <url>");
+    Utils.assert(callback, "invalid or missing argument <callback>");
 
     let self = this;
 
-    let completed = function(status, imageData) {
-      callback(status, imageData);
-
-      if (status) {
-        // Notify subscribers
-        tab._tabViewTabItem._sendToSubscribers("loadedCachedImageData");
-      } else {
-        Utils.log("Error while loading thumbnail");
-      }
-    }
-
-    let onCacheEntryAvailable = function(entry) {
+    function onCacheEntryAvailable(entry) {
       let imageChunks = [];
       let nativeInputStream = entry.openInputStream(0);
 
       const CHUNK_SIZE = 0x10000; // 65k
       const PR_UINT32_MAX = 0xFFFFFFFF;
       let storageStream = new self._storageStream(CHUNK_SIZE, PR_UINT32_MAX, null);
       let storageOutStream = storageStream.getOutputStream(0);
 
@@ -223,26 +221,26 @@ let ThumbnailStorage = {
         if (isSuccess) {
           let storageInStream = storageStream.newInputStream(0);
           imageData = gNetUtil.readInputStreamToString(storageInStream,
             storageInStream.available());
           storageInStream.close();
         }
 
         cleanup();
-        completed(isSuccess, imageData);
+        callback(isSuccess ? "" : "failure", imageData);
       });
     }
 
-    let onCacheEntryUnavailable = function() {
-      completed(false);
+    function onCacheEntryUnavailable() {
+      callback("unavailable");
     }
 
-    this._openCacheEntry(url, Ci.nsICache.ACCESS_READ,
-        onCacheEntryAvailable, onCacheEntryUnavailable);
+    this._openCacheEntry(url, Ci.nsICache.ACCESS_READ, onCacheEntryAvailable,
+                         onCacheEntryUnavailable);
   }
 }
 
 // ##########
 // Class: CacheListener
 // Generic CacheListener for feeding to asynchronous cache calls.
 // Calls <callback>(entry, access, status) when the requested cache entry
 // is available.
--- a/browser/base/content/tabview/ui.js
+++ b/browser/base/content/tabview/ui.js
@@ -278,23 +278,26 @@ let UI = {
       iQ(window).resize(function() {
         self._resize();
       });
 
       // ___ setup event listener to save canvas images
       gWindow.addEventListener("SSWindowClosing", function onWindowClosing() {
         gWindow.removeEventListener("SSWindowClosing", onWindowClosing, false);
 
+        // XXX bug #635975 - don't unlink the tab if the dom window is closing.
         self.isDOMWindowClosing = true;
 
         if (self.isTabViewVisible())
           GroupItems.removeHiddenGroups();
 
+        TabItems.saveAll();
+        TabItems.saveAllThumbnails({synchronously: true});
+
         Storage.saveActiveGroupName(gWindow);
-        TabItems.saveAll(true);
         self._save();
       }, false);
 
       // ___ load frame script
       let frameScript = "chrome://browser/content/tabview-content.js";
       gWindow.messageManager.loadFrameScript(frameScript, true);
 
       // ___ Done
@@ -711,16 +714,21 @@ let UI = {
           self._privateBrowsing.wasInTabView = self.isTabViewVisible();
           if (self.isTabViewVisible())
             self.goToTab(gBrowser.selectedTab);
         }
       } else if (topic == "private-browsing-change-granted") {
         if (data == "enter" || data == "exit") {
           hideSearch();
           self._privateBrowsing.transitionMode = data;
+
+          // make sure to save all thumbnails that haven't been saved yet
+          // before we enter the private browsing mode
+          if (data == "enter")
+            TabItems.saveAllThumbnails({synchronously: true});
         }
       } else if (topic == "private-browsing-transition-complete") {
         // We use .transitionMode here, as aData is empty.
         if (self._privateBrowsing.transitionMode == "exit" &&
             self._privateBrowsing.wasInTabView)
           self.showTabView(false);
 
         self._privateBrowsing.transitionMode = "";
--- a/browser/base/content/test/tabview/Makefile.in
+++ b/browser/base/content/test/tabview/Makefile.in
@@ -77,17 +77,16 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug597980.js \
                  browser_tabview_bug598375.js \
                  browser_tabview_bug598600.js \
                  browser_tabview_bug599626.js \
                  browser_tabview_bug600645.js \
                  browser_tabview_bug600812.js \
                  browser_tabview_bug602432.js \
                  browser_tabview_bug604098.js \
-                 browser_tabview_bug604699.js \
                  browser_tabview_bug606657.js \
                  browser_tabview_bug606905.js \
                  browser_tabview_bug607108.js \
                  browser_tabview_bug608037.js \
                  browser_tabview_bug608184.js \
                  browser_tabview_bug608158.js \
                  browser_tabview_bug608405.js \
                  browser_tabview_bug610208.js \
@@ -149,32 +148,34 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug656778.js \
                  browser_tabview_bug656913.js \
                  browser_tabview_bug662266.js \
                  browser_tabview_bug663421.js \
                  browser_tabview_bug665502.js \
                  browser_tabview_bug669694.js \
                  browser_tabview_bug673196.js \
                  browser_tabview_bug673729.js \
+                 browser_tabview_bug677310.js \
                  browser_tabview_bug679853.js \
                  browser_tabview_bug681599.js \
                  browser_tabview_click_group.js \
                  browser_tabview_dragdrop.js \
                  browser_tabview_exit_button.js \
                  browser_tabview_expander.js \
                  browser_tabview_firstrun_pref.js \
                  browser_tabview_group.js \
                  browser_tabview_launch.js \
                  browser_tabview_multiwindow_search.js \
                  browser_tabview_privatebrowsing.js \
                  browser_tabview_rtl.js \
                  browser_tabview_search.js \
                  browser_tabview_snapping.js \
                  browser_tabview_startup_transitions.js \
                  browser_tabview_storage_policy.js \
+                 browser_tabview_thumbnail_storage.js \
                  browser_tabview_undo_group.js \
                  dummy_page.html \
                  head.js \
                  search1.html \
                  search2.html \
                  test_bug600645.html \
                  test_bug644097.html \
                  $(NULL)
--- a/browser/base/content/test/tabview/browser_tabview_bug597248.js
+++ b/browser/base/content/test/tabview/browser_tabview_bug597248.js
@@ -29,17 +29,19 @@ function setupTwo(win) {
 
   let tabItems = contentWindow.TabItems.getItems();
   is(tabItems.length, 3, "There should be 3 tab items before closing");
 
   let numTabsToSave = tabItems.length;
 
   // force all canvases to update, and hook in imageData save detection
   tabItems.forEach(function(tabItem) {
-    contentWindow.TabItems.update(tabItem.tab);
+    // mark thumbnail as dirty
+    tabItem.tabCanvas.paint();
+
     tabItem.addSubscriber("savedCachedImageData", function onSaved(item) {
       item.removeSubscriber("savedCachedImageData", onSaved);
 
       if (!--numTabsToSave)
         restoreWindow();
     });
   });
 
@@ -76,18 +78,18 @@ function setupTwo(win) {
             else
               frameInitialized = true;
           }
 
           let tabItems = restoredContentWindow.TabItems.getItems();
           let count = tabItems.length;
 
           tabItems.forEach(function(tabItem) {
-            tabItem.addSubscriber("loadedCachedImageData", function onLoaded() {
-              tabItem.removeSubscriber("loadedCachedImageData", onLoaded);
+            tabItem.addSubscriber("showingCachedData", function onLoaded() {
+              tabItem.removeSubscriber("showingCachedData", onLoaded);
               ok(tabItem.isShowingCachedData(),
                 "Tab item is showing cached data and is just connected. " +
                 tabItem.tab.linkedBrowser.currentURI.spec);
               if (--count == 0)
                 nextStep();
             });
           });
         }
deleted file mode 100644
--- a/browser/base/content/test/tabview/browser_tabview_bug604699.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-function test() {
-  let url = "http://www.example.com/";
-  let cw;
-  let tab = gBrowser.tabs[0];
-
-  let finishTest = function () {
-    is(1, gBrowser.tabs.length, "there is one tab, only");
-    ok(!TabView.isVisible(), "tabview is not visible");
-    finish();
-  }
-
-  waitForExplicitFinish();
-
-  let testErroneousLoading = function () {
-    cw.ThumbnailStorage.loadThumbnail(tab, url, function (status, data) {
-      ok(!status, "thumbnail entry failed to load");
-      is(null, data, "no thumbnail data received");
-      next();
-    });
-  }
-
-  let testAsynchronousSaving = function () {
-    let saved = false;
-    let data = "thumbnail-data-asynchronous";
-
-    cw.ThumbnailStorage.saveThumbnail(tab, data, function (status) {
-      ok(status, "thumbnail entry was saved");
-      ok(saved, "thumbnail was saved asynchronously");
-
-      cw.ThumbnailStorage.loadThumbnail(tab, url, function (status, imageData) {
-        ok(status, "thumbnail entry was loaded");
-        is(imageData, data, "valid thumbnail data received");
-        next();
-      });
-    });
-
-    saved = true;
-  }
-
-  let testSynchronousSaving = function () {
-    let saved = false;
-    let data = "thumbnail-data-synchronous";
-
-    cw.UI.isDOMWindowClosing = true;
-    registerCleanupFunction(function () cw.UI.isDOMWindowClosing = false);
-
-    cw.ThumbnailStorage.saveThumbnail(tab, data, function (status) {
-      ok(status, "thumbnail entry was saved");
-      ok(!saved, "thumbnail was saved synchronously");
-
-      cw.ThumbnailStorage.loadThumbnail(tab, url, function (status, imageData) {
-        ok(status, "thumbnail entry was loaded");
-        is(imageData, data, "valid thumbnail data received");
-
-        cw.UI.isDOMWindowClosing = false;
-        next();
-      });
-    });
-
-    saved = true;
-  }
-
-  let tests = [testErroneousLoading, testAsynchronousSaving, testSynchronousSaving];
-
-  let next = function () {
-    let test = tests.shift();
-    if (test)
-      test();
-    else
-      hideTabView(finishTest);
-  }
-
-  tab.linkedBrowser.loadURI(url);
-  afterAllTabsLoaded(function() {
-    showTabView(function () {
-      registerCleanupFunction(function () TabView.hide());
-      cw = TabView.getContentWindow();
-
-      next();
-    });
-  });
-}
--- a/browser/base/content/test/tabview/browser_tabview_bug627288.js
+++ b/browser/base/content/test/tabview/browser_tabview_bug627288.js
@@ -17,27 +17,28 @@ function test() {
       tab = gBrowser.loadOneTab('http://mochi.test:8888/', {inBackground: true});
 
       afterAllTabsLoaded(function () {
         tabItem = tab._tabViewTabItem;
 
         tabItem.addSubscriber("savedCachedImageData", function onSaved() {
           tabItem.removeSubscriber("savedCachedImageData", onSaved);
 
-          tabItem.addSubscriber("loadedCachedImageData", function onLoaded() {
-            tabItem.removeSubscriber("loadedCachedImageData", onLoaded);
+          tabItem.addSubscriber("showingCachedData", function onLoaded() {
+            tabItem.removeSubscriber("showingCachedData", onLoaded);
 
             ok(tabItem.isShowingCachedData(), 'tabItem shows cached data');
             testChangeUrlAfterReconnect();
           });
 
           cw.TabItems.resumeReconnecting();
         });
 
         cw.Storage.saveTab(tab, data);
+        tabItem.saveThumbnail();
       });
     });
   }
 
   let testChangeUrlAfterReconnect = function () {
     tab.linkedBrowser.loadURI('http://mochi.test:8888/browser/');
 
     whenTabAttrModified(tab, function () {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_bug677310.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let pb = Cc["@mozilla.org/privatebrowsing;1"].
+         getService(Ci.nsIPrivateBrowsingService);
+
+function test() {
+  let thumbnailsSaved = false;
+
+  waitForExplicitFinish();
+
+  registerCleanupFunction(function () {
+    ok(thumbnailsSaved, "thumbs have been saved before entering pb mode");
+    pb.privateBrowsingEnabled = false;
+  });
+
+  afterAllTabsLoaded(function () {
+    showTabView(function () {
+      hideTabView(function () {
+        let numConditions = 2;
+
+        function check() {
+          if (--numConditions)
+            return;
+
+          togglePrivateBrowsing(finish);
+        }
+
+        let tabItem = gBrowser.tabs[0]._tabViewTabItem;
+
+        // save all thumbnails synchronously to cancel all delayed thumbnail
+        // saves that might be active
+        tabItem.saveThumbnail({synchronously: true});
+
+        // force a tabCanvas paint to flag the thumbnail as dirty
+        tabItem.tabCanvas.paint();
+
+        tabItem.addSubscriber("savedCachedImageData", function onSaved() {
+          tabItem.removeSubscriber("savedCachedImageData", onSaved);
+          thumbnailsSaved = true;
+          check();
+        });
+
+        togglePrivateBrowsing(check);
+      });
+    });
+  });
+}
--- a/browser/base/content/test/tabview/browser_tabview_storage_policy.js
+++ b/browser/base/content/test/tabview/browser_tabview_storage_policy.js
@@ -34,18 +34,18 @@ function test1() {
   HttpRequestObserver.cacheControlValue = "no-store";
 
   whenStorageDenied(newTab, function () {
     let tabItem = newTab._tabViewTabItem;
 
     ok(!contentWindow.StoragePolicy.canStoreThumbnailForTab(newTab), 
        "Should not save the thumbnail for tab");
 
-    whenDeniedToCacheImageData(tabItem, test2);
-    tabItem.save(true);
+    whenDeniedToSaveImageData(tabItem, test2);
+    tabItem.saveThumbnail({synchronously: true});
     HttpRequestObserver.cacheControlValue = null;
   });
 
   newTab.linkedBrowser.loadURI("http://www.example.com/browser/browser/base/content/test/tabview/dummy_page.html");
 }
 
 function test2() {
   // page with cache-control: private, should save thumbnail
@@ -54,17 +54,17 @@ function test2() {
   newTab.linkedBrowser.loadURI("http://www.example.com/");
   afterAllTabsLoaded(function() {
     let tabItem = newTab._tabViewTabItem;
 
     ok(contentWindow.StoragePolicy.canStoreThumbnailForTab(newTab), 
        "Should save the thumbnail for tab");
 
     whenSavedCachedImageData(tabItem, test3);
-    tabItem.save(true);
+    tabItem.saveThumbnail({synchronously: true});
   });
 }
 
 function test3() {
   // page with cache-control: private with https caching enabled, should save thumbnail
   HttpRequestObserver.cacheControlValue = "private";
 
   Services.prefs.setBoolPref(PREF_DISK_CACHE_SSL, true);
@@ -72,17 +72,17 @@ function test3() {
   newTab.linkedBrowser.loadURI("https://example.com/browser/browser/base/content/test/tabview/dummy_page.html");
   afterAllTabsLoaded(function() {
     let tabItem = newTab._tabViewTabItem;
 
     ok(contentWindow.StoragePolicy.canStoreThumbnailForTab(newTab),
        "Should save the thumbnail for tab");
 
     whenSavedCachedImageData(tabItem, test4);
-    tabItem.save(true);
+    tabItem.saveThumbnail({synchronously: true});
   });
 }
 
 function test4() {
   // page with cache-control: public with https caching disabled, should save thumbnail
   HttpRequestObserver.cacheControlValue = "public";
 
   Services.prefs.setBoolPref(PREF_DISK_CACHE_SSL, false);
@@ -90,37 +90,37 @@ function test4() {
   newTab.linkedBrowser.loadURI("https://example.com/browser/browser/base/content/test/tabview/");
   afterAllTabsLoaded(function() {
     let tabItem = newTab._tabViewTabItem;
 
     ok(contentWindow.StoragePolicy.canStoreThumbnailForTab(newTab),
        "Should save the thumbnail for tab");
 
     whenSavedCachedImageData(tabItem, test5);
-    tabItem.save(true);
+    tabItem.saveThumbnail({synchronously: true});
   });
 }
 
 function test5() {
   // page with cache-control: private with https caching disabled, should not save thumbnail
   HttpRequestObserver.cacheControlValue = "private";
 
   whenStorageDenied(newTab, function () {
     let tabItem = newTab._tabViewTabItem;
 
     ok(!contentWindow.StoragePolicy.canStoreThumbnailForTab(newTab),
        "Should not save the thumbnail for tab");
 
-    whenDeniedToCacheImageData(tabItem, function () {
+    whenDeniedToSaveImageData(tabItem, function () {
       hideTabView(function () {
         gBrowser.removeTab(gBrowser.tabs[1]);
         finish();
       });
     });
-    tabItem.save(true);
+    tabItem.saveThumbnail({synchronously: true});
   });
 
   newTab.linkedBrowser.loadURI("https://example.com/");
 }
 
 let HttpRequestObserver = {
   cacheControlValue: null,
 
@@ -142,19 +142,19 @@ let HttpRequestObserver = {
 
 function whenSavedCachedImageData(tabItem, callback) {
   tabItem.addSubscriber("savedCachedImageData", function onSaved() {
     tabItem.removeSubscriber("savedCachedImageData", onSaved);
     callback();
   });
 }
 
-function whenDeniedToCacheImageData(tabItem, callback) {
-  tabItem.addSubscriber("deniedToCacheImageData", function onDenied() {
-    tabItem.removeSubscriber("deniedToCacheImageData", onDenied);
+function whenDeniedToSaveImageData(tabItem, callback) {
+  tabItem.addSubscriber("deniedToSaveImageData", function onDenied() {
+    tabItem.removeSubscriber("deniedToSaveImageData", onDenied);
     callback();
   });
 }
 
 function whenStorageDenied(tab, callback) {
   let mm = tab.linkedBrowser.messageManager;
 
   mm.addMessageListener("Panorama:StoragePolicy:denied", function onDenied() {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_thumbnail_storage.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let tests = [testRawSyncSave, testRawAsyncSave, testRawLoadError,
+             testAsyncSave, testSyncSave, testOverrideAsyncSave,
+             testSaveCleanThumbnail];
+
+function test() {
+  waitForExplicitFinish();
+  loadTabView(next);
+}
+
+function testRawSyncSave() {
+  let cw = TabView.getContentWindow();
+  let url = "http://example.com/sync-url";
+  let data = "thumbnail-data-sync";
+  let saved = false;
+
+  cw.ThumbnailStorage.saveThumbnail(url, data, function (error) {
+    ok(!error, "thumbnail entry was saved");
+    ok(!saved, "thumbnail was saved synchronously");
+
+    cw.ThumbnailStorage.loadThumbnail(url, function (error, imageData) {
+      ok(!error, "thumbnail entry was loaded");
+      is(imageData, data, "valid thumbnail data received");
+      next();
+    });
+  }, {synchronously: true});
+
+  saved = true;
+}
+
+function testRawAsyncSave() {
+  let cw = TabView.getContentWindow();
+  let url = "http://example.com/async-url";
+  let data = "thumbnail-data-async";
+  let saved = false;
+
+  cw.ThumbnailStorage.saveThumbnail(url, data, function (error) {
+    ok(!error, "thumbnail entry was saved");
+    ok(saved, "thumbnail was saved asynchronously");
+
+    cw.ThumbnailStorage.loadThumbnail(url, function (error, imageData) {
+      ok(!error, "thumbnail entry was loaded");
+      is(imageData, data, "valid thumbnail data received");
+      next();
+    });
+  });
+
+  saved = true;
+}
+
+function testRawLoadError() {
+  let cw = TabView.getContentWindow();
+
+  cw.ThumbnailStorage.loadThumbnail("non-existant-url", function (error, data) {
+    ok(error, "thumbnail entry failed to load");
+    is(null, data, "no thumbnail data received");
+    next();
+  });
+}
+
+function testSyncSave() {
+  let tabItem = gBrowser.tabs[0]._tabViewTabItem;
+
+  // set the thumbnail to dirty
+  tabItem.tabCanvas.paint();
+
+  let saved = false;
+
+  whenThumbnailSaved(tabItem, function () {
+    ok(!saved, "thumbnail was saved synchronously");
+    next();
+  });
+
+  tabItem.saveThumbnail({synchronously: true});
+  saved = true;
+}
+
+function testAsyncSave() {
+  let tabItem = gBrowser.tabs[0]._tabViewTabItem;
+
+  // set the thumbnail to dirty
+  tabItem.tabCanvas.paint();
+
+  let saved = false;
+
+  whenThumbnailSaved(tabItem, function () {
+    ok(saved, "thumbnail was saved asynchronously");
+    next();
+  });
+
+  tabItem.saveThumbnail();
+  saved = true;
+}
+
+function testOverrideAsyncSave() {
+  let tabItem = gBrowser.tabs[0]._tabViewTabItem;
+
+  // set the thumbnail to dirty
+  tabItem.tabCanvas.paint();
+
+  // initiate async save
+  tabItem.saveThumbnail();
+
+  let saveCount = 0;
+
+  whenThumbnailSaved(tabItem, function () {
+    saveCount = 1;
+  });
+
+  tabItem.saveThumbnail({synchronously: true});
+
+  is(saveCount, 1, "thumbnail got saved once");
+  next();
+}
+
+function testSaveCleanThumbnail() {
+  let tabItem = gBrowser.tabs[0]._tabViewTabItem;
+
+  // set the thumbnail to dirty
+  tabItem.tabCanvas.paint();
+
+  let saveCount = 0;
+
+  whenThumbnailSaved(tabItem, function () saveCount++);
+  tabItem.saveThumbnail({synchronously: true});
+  tabItem.saveThumbnail({synchronously: true});
+
+  is(saveCount, 1, "thumbnail got saved once, only");
+  next();
+}
+
+// ----------
+function whenThumbnailSaved(tabItem, callback) {
+  tabItem.addSubscriber("savedCachedImageData", function onSaved() {
+    tabItem.removeSubscriber("savedCachedImageData", onSaved);
+    callback();
+  });
+}
+
+// ----------
+function loadTabView(callback) {
+  afterAllTabsLoaded(function () {
+    showTabView(function () {
+      hideTabView(callback);
+    });
+  });
+}
+
+// ----------
+function next() {
+  let test = tests.shift();
+
+  if (test) {
+    info("* running " + test.name + "...");
+    test();
+  } else {
+    finish();
+  }
+}