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 77701 b5bb731a58a9a823f6a0f458343c5f454a0afbae
parent 77700 63becbe85737890d364f39cf65188cbceca3c93b
child 77702 b7a7127396f63ac9400ae382efe87314db64339c
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdietrich
bugs677310
milestone9.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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();
+  }
+}