783 lines
24 KiB
JavaScript
783 lines
24 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms
|
|
// For testing, the above timeout is excessive, and makes our tests overlong.
|
|
const TESTING_CAPTURE_TIMEOUT = 5000; // ms
|
|
|
|
const DESTROY_BROWSER_TIMEOUT = 60000; // ms
|
|
|
|
// Let the page settle for this amount of milliseconds before capturing to allow
|
|
// for any in-page changes or redirects.
|
|
const SETTLE_WAIT_TIME = 2500;
|
|
// For testing, the above timeout is excessive, and makes our tests overlong.
|
|
const TESTING_SETTLE_WAIT_TIME = 0;
|
|
|
|
const ABOUT_NEWTAB_SEGREGATION_PREF =
|
|
"privacy.usercontext.about_newtab_segregation.enabled";
|
|
|
|
import {
|
|
PageThumbs,
|
|
PageThumbsStorage,
|
|
} from "resource://gre/modules/PageThumbs.sys.mjs";
|
|
|
|
// possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2 telemetry values
|
|
const TEL_CAPTURE_DONE_OK = 0;
|
|
const TEL_CAPTURE_DONE_TIMEOUT = 1;
|
|
// 2 and 3 were used when we had special handling for private-browsing.
|
|
const TEL_CAPTURE_DONE_CRASHED = 4;
|
|
const TEL_CAPTURE_DONE_BAD_URI = 5;
|
|
const TEL_CAPTURE_DONE_LOAD_FAILED = 6;
|
|
const TEL_CAPTURE_DONE_IMAGE_ZERO_DIMENSION = 7;
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ContextualIdentityService:
|
|
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
|
});
|
|
|
|
export const BackgroundPageThumbs = {
|
|
/**
|
|
* Asynchronously captures a thumbnail of the given URL.
|
|
*
|
|
* The page is loaded anonymously, and plug-ins are disabled.
|
|
*
|
|
* @param url The URL to capture.
|
|
* @param options An optional object that configures the capture. Its
|
|
* properties are the following, and all are optional:
|
|
* @opt onDone A function that will be asynchronously called when the
|
|
* capture is complete or times out. It's called as
|
|
* onDone(url),
|
|
* where `url` is the captured URL.
|
|
* @opt timeout The capture will time out after this many milliseconds have
|
|
* elapsed after the capture has progressed to the head of
|
|
* the queue and started. Defaults to 30000 (30 seconds).
|
|
* @opt isImage If true, backgroundPageThumbsContent will attempt to render
|
|
* the url directly to canvas. Note that images will mostly get
|
|
* detected and rendered as such anyway, but this will ensure it.
|
|
* @opt targetWidth The target width when capturing an image.
|
|
* @opt backgroundColor The background colour when capturing an image.
|
|
* @opt dontStore If set to true, the image blob won't be stored to disk, an
|
|
* object will instead be passed as third argument to onDone:
|
|
* {
|
|
* data: an ArrayBuffer containing the data
|
|
* contentType: the data content-type
|
|
* originalUrl: the originally requested url
|
|
* currentUrl: the final url after redirects
|
|
* }
|
|
* @opt contentType can be set to an image contentType for the capture,
|
|
* defaults to PageThumbs.contentType.
|
|
*/
|
|
capture(url, options = {}) {
|
|
if (!PageThumbs._prefEnabled()) {
|
|
if (options.onDone) {
|
|
schedule(() => options.onDone(url));
|
|
}
|
|
return;
|
|
}
|
|
this._captureQueue = this._captureQueue || [];
|
|
this._capturesByURL = this._capturesByURL || new Map();
|
|
|
|
Glean.thumbnails.queueSizeOnCapture.accumulateSingleSample(
|
|
this._captureQueue.length
|
|
);
|
|
|
|
// We want to avoid duplicate captures for the same URL. If there is an
|
|
// existing one, we just add the callback to that one and we are done.
|
|
let existing = this._capturesByURL.get(url);
|
|
if (existing) {
|
|
if (options.onDone) {
|
|
existing.doneCallbacks.push(options.onDone);
|
|
}
|
|
// The queue is already being processed, so nothing else to do...
|
|
return;
|
|
}
|
|
let cap = new Capture(url, this._onCaptureOrTimeout.bind(this), options);
|
|
this._captureQueue.push(cap);
|
|
this._capturesByURL.set(url, cap);
|
|
this._processCaptureQueue();
|
|
},
|
|
|
|
/**
|
|
* Asynchronously captures a thumbnail of the given URL if one does not
|
|
* already exist. Otherwise does nothing.
|
|
*
|
|
* @param url The URL to capture.
|
|
* @param options An optional object that configures the capture. See
|
|
* capture() for description.
|
|
* unloadingPromise This option is resolved when the calling context is
|
|
* unloading, so things can be cleaned up to avoid leak.
|
|
* @return {Promise} A Promise that resolves when this task completes
|
|
*/
|
|
async captureIfMissing(url, options = {}) {
|
|
// Short circuit this function if pref is enabled, or else we leak observers.
|
|
// See Bug 1400562
|
|
if (!PageThumbs._prefEnabled()) {
|
|
if (options.onDone) {
|
|
options.onDone(url);
|
|
}
|
|
return url;
|
|
}
|
|
// The fileExistsForURL call is an optimization, potentially but unlikely
|
|
// incorrect, and no big deal when it is. After the capture is done, we
|
|
// atomically test whether the file exists before writing it.
|
|
let exists = await PageThumbsStorage.fileExistsForURL(url);
|
|
if (exists) {
|
|
if (options.onDone) {
|
|
options.onDone(url);
|
|
}
|
|
return url;
|
|
}
|
|
let thumbPromise = new Promise((resolve, reject) => {
|
|
let observe = (subject, topic, data) => {
|
|
if (data === url) {
|
|
switch (topic) {
|
|
case "page-thumbnail:create":
|
|
resolve();
|
|
break;
|
|
case "page-thumbnail:error":
|
|
reject(new Error("page-thumbnail:error"));
|
|
break;
|
|
}
|
|
cleanup();
|
|
}
|
|
};
|
|
Services.obs.addObserver(observe, "page-thumbnail:create");
|
|
Services.obs.addObserver(observe, "page-thumbnail:error");
|
|
|
|
// Make sure to clean up to avoid leaks by removing observers when
|
|
// observed or when our caller is unloading
|
|
function cleanup() {
|
|
if (observe) {
|
|
Services.obs.removeObserver(observe, "page-thumbnail:create");
|
|
Services.obs.removeObserver(observe, "page-thumbnail:error");
|
|
observe = null;
|
|
}
|
|
}
|
|
if (options.unloadingPromise) {
|
|
options.unloadingPromise.then(cleanup);
|
|
}
|
|
});
|
|
try {
|
|
this.capture(url, options);
|
|
await thumbPromise;
|
|
} catch (err) {
|
|
if (options.onDone) {
|
|
options.onDone(url);
|
|
}
|
|
throw err;
|
|
}
|
|
return url;
|
|
},
|
|
|
|
/**
|
|
* Tell the service that the thumbnail browser should be recreated at next
|
|
* call of _ensureBrowser().
|
|
*/
|
|
renewThumbnailBrowser() {
|
|
this._renewThumbBrowser = true;
|
|
},
|
|
|
|
get useFissionBrowser() {
|
|
return Services.appinfo.fissionAutostart;
|
|
},
|
|
|
|
/**
|
|
* Ensures that initialization of the thumbnail browser's parent window has
|
|
* begun.
|
|
*
|
|
* @return True if the parent window is completely initialized and can be
|
|
* used, and false if initialization has started but not completed.
|
|
*/
|
|
_ensureParentWindowReady() {
|
|
if (this._parentWin) {
|
|
// Already fully initialized.
|
|
return true;
|
|
}
|
|
if (this._startedParentWinInit) {
|
|
// Already started initializing.
|
|
return false;
|
|
}
|
|
|
|
this._startedParentWinInit = true;
|
|
|
|
// Create a windowless browser and load our hosting
|
|
// (privileged) document in it.
|
|
const flags = this.useFissionBrowser
|
|
? Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW |
|
|
Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW
|
|
: 0;
|
|
let wlBrowser = Services.appShell.createWindowlessBrowser(true, flags);
|
|
wlBrowser.QueryInterface(Ci.nsIInterfaceRequestor);
|
|
let webProgress = wlBrowser.getInterface(Ci.nsIWebProgress);
|
|
this._listener = {
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIWebProgressListener",
|
|
"nsIWebProgressListener2",
|
|
"nsISupportsWeakReference",
|
|
]),
|
|
};
|
|
this._listener.onStateChange = (wbp, request, stateFlags) => {
|
|
if (!request) {
|
|
return;
|
|
}
|
|
if (
|
|
stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
|
|
stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
|
|
) {
|
|
webProgress.removeProgressListener(this._listener);
|
|
delete this._listener;
|
|
// Get the window reference via the document.
|
|
this._parentWin = wlBrowser.document.defaultView;
|
|
this._processCaptureQueue();
|
|
}
|
|
};
|
|
webProgress.addProgressListener(
|
|
this._listener,
|
|
Ci.nsIWebProgress.NOTIFY_STATE_ALL
|
|
);
|
|
let loadURIOptions = {
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
};
|
|
wlBrowser.loadURI(
|
|
Services.io.newURI("chrome://global/content/backgroundPageThumbs.xhtml"),
|
|
loadURIOptions
|
|
);
|
|
this._windowlessContainer = wlBrowser;
|
|
|
|
return false;
|
|
},
|
|
|
|
_init() {
|
|
Services.prefs.addObserver(ABOUT_NEWTAB_SEGREGATION_PREF, this);
|
|
Services.obs.addObserver(this, "profile-before-change");
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic == "profile-before-change") {
|
|
this._destroy();
|
|
} else if (
|
|
topic == "nsPref:changed" &&
|
|
data == ABOUT_NEWTAB_SEGREGATION_PREF
|
|
) {
|
|
BackgroundPageThumbs.renewThumbnailBrowser();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Destroys the service. Queued and pending captures will never complete, and
|
|
* their consumer callbacks will never be called.
|
|
*/
|
|
_destroy() {
|
|
if (this._captureQueue) {
|
|
this._captureQueue.forEach(cap => cap.destroy());
|
|
}
|
|
this._destroyBrowser();
|
|
if (this._windowlessContainer) {
|
|
this._windowlessContainer.close();
|
|
}
|
|
delete this._captureQueue;
|
|
delete this._windowlessContainer;
|
|
delete this._startedParentWinInit;
|
|
delete this._parentWin;
|
|
delete this._listener;
|
|
},
|
|
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
Ci.nsIWebProgressListener,
|
|
Ci.nsIWebProgressListener2,
|
|
Ci.nsISupportsWeakReference,
|
|
]),
|
|
|
|
onStateChange(wbp, request, stateFlags, status) {
|
|
if (!request || !wbp.isTopLevel) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
|
|
stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
|
|
) {
|
|
// If about:blank is being loaded after a capture, move on
|
|
// to the next capture, otherwise ignore about:blank loads.
|
|
if (
|
|
request instanceof Ci.nsIChannel &&
|
|
request.URI.spec == "about:blank"
|
|
) {
|
|
if (this._expectingAboutBlank) {
|
|
this._expectingAboutBlank = false;
|
|
if (this._captureQueue.length) {
|
|
this._processCaptureQueue();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!this._captureQueue.length) {
|
|
return;
|
|
}
|
|
|
|
let currentCapture = this._captureQueue[0];
|
|
if (
|
|
Components.isSuccessCode(status) ||
|
|
status === Cr.NS_BINDING_ABORTED
|
|
) {
|
|
this._thumbBrowser.ownerGlobal.requestIdleCallback(() => {
|
|
currentCapture.pageLoaded(this._thumbBrowser);
|
|
});
|
|
} else {
|
|
currentCapture._done(
|
|
this._thumbBrowser,
|
|
null,
|
|
currentCapture.timedOut
|
|
? TEL_CAPTURE_DONE_TIMEOUT
|
|
: TEL_CAPTURE_DONE_LOAD_FAILED
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Creates the thumbnail browser if it doesn't already exist.
|
|
*/
|
|
_ensureBrowser() {
|
|
if (this._thumbBrowser && !this._renewThumbBrowser) {
|
|
return;
|
|
}
|
|
|
|
this._destroyBrowser();
|
|
this._renewThumbBrowser = false;
|
|
|
|
let browser = this._parentWin.document.createXULElement("browser");
|
|
browser.setAttribute("type", "content");
|
|
browser.setAttribute("remote", "true");
|
|
if (this.useFissionBrowser) {
|
|
browser.setAttribute("maychangeremoteness", "true");
|
|
}
|
|
browser.setAttribute("disableglobalhistory", "true");
|
|
browser.setAttribute("messagemanagergroup", "thumbnails");
|
|
browser.setAttribute("manualactiveness", "true");
|
|
|
|
if (Services.prefs.getBoolPref(ABOUT_NEWTAB_SEGREGATION_PREF)) {
|
|
// Use the private container for thumbnails.
|
|
let privateIdentity = lazy.ContextualIdentityService.getPrivateIdentity(
|
|
"userContextIdInternal.thumbnail"
|
|
);
|
|
browser.setAttribute("usercontextid", privateIdentity.userContextId);
|
|
}
|
|
|
|
// Size the browser. Make its aspect ratio the same as the canvases' that
|
|
// the thumbnails are drawn into; the canvases' aspect ratio is the same as
|
|
// the screen's, so use that. Aim for a size in the ballpark of 1024x768.
|
|
let [swidth, sheight] = [{}, {}];
|
|
Cc["@mozilla.org/gfx/screenmanager;1"]
|
|
.getService(Ci.nsIScreenManager)
|
|
.primaryScreen.GetRectDisplayPix({}, {}, swidth, sheight);
|
|
let bwidth = Math.min(1024, swidth.value);
|
|
// Setting the width and height attributes doesn't work -- the resulting
|
|
// thumbnails are blank and transparent -- but setting the style does.
|
|
browser.style.width = bwidth + "px";
|
|
browser.style.height = (bwidth * sheight.value) / swidth.value + "px";
|
|
browser.style.colorScheme = "env(-moz-content-preferred-color-scheme)";
|
|
|
|
this._parentWin.document.documentElement.appendChild(browser);
|
|
|
|
browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
|
|
browser.mute();
|
|
|
|
// an event that is sent if the remote process crashes - no need to remove
|
|
// it as we want it to be there as long as the browser itself lives.
|
|
browser.addEventListener("oop-browser-crashed", event => {
|
|
if (!event.isTopFrame) {
|
|
// It was a subframe that crashed. We'll ignore this.
|
|
return;
|
|
}
|
|
|
|
console.error("BackgroundThumbnails remote process crashed - recovering");
|
|
this._destroyBrowser();
|
|
let curCapture = this._captureQueue.length ? this._captureQueue[0] : null;
|
|
// we could retry the pending capture, but it's possible the crash
|
|
// was due directly to it, so trying again might just crash again.
|
|
// We could keep a flag to indicate if it previously crashed, but
|
|
// "resetting" the capture requires more work - so for now, we just
|
|
// discard it.
|
|
if (curCapture) {
|
|
// Continue queue processing by calling curCapture._done(). Do it after
|
|
// this crashed listener returns, though. A new browser will be created
|
|
// immediately (on the same stack as the _done call stack) if there are
|
|
// any more queued-up captures, and that seems to mess up the new
|
|
// browser's message manager if it happens on the same stack as the
|
|
// listener. Trying to send a message to the manager in that case
|
|
// throws NS_ERROR_NOT_INITIALIZED.
|
|
Services.tm.dispatchToMainThread(() => {
|
|
curCapture._done(browser, null, TEL_CAPTURE_DONE_CRASHED);
|
|
});
|
|
}
|
|
// else: we must have been idle and not currently doing a capture (eg,
|
|
// maybe a GC or similar crashed) - so there's no need to attempt a
|
|
// queue restart - the next capture request will set everything up.
|
|
});
|
|
|
|
this._thumbBrowser = browser;
|
|
browser.docShellIsActive = false;
|
|
},
|
|
|
|
_destroyBrowser() {
|
|
if (!this._thumbBrowser) {
|
|
return;
|
|
}
|
|
this._expectingAboutBlank = false;
|
|
this._thumbBrowser.remove();
|
|
delete this._thumbBrowser;
|
|
},
|
|
|
|
async _loadAboutBlank() {
|
|
if (this._expectingAboutBlank) {
|
|
return;
|
|
}
|
|
|
|
this._expectingAboutBlank = true;
|
|
|
|
let loadURIOptions = {
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT,
|
|
};
|
|
this._thumbBrowser.loadURI(
|
|
Services.io.newURI("about:blank"),
|
|
loadURIOptions
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Starts the next capture if the queue is not empty and the service is fully
|
|
* initialized.
|
|
*/
|
|
_processCaptureQueue() {
|
|
if (!this._captureQueue.length) {
|
|
if (this._thumbBrowser) {
|
|
BackgroundPageThumbs._loadAboutBlank();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this._captureQueue[0].pending ||
|
|
!this._ensureParentWindowReady() ||
|
|
this._expectingAboutBlank
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Ready to start the first capture in the queue.
|
|
this._ensureBrowser();
|
|
this._captureQueue[0].start(this._thumbBrowser);
|
|
if (this._destroyBrowserTimer) {
|
|
this._destroyBrowserTimer.cancel();
|
|
delete this._destroyBrowserTimer;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when the current capture completes or fails (eg, times out, remote
|
|
* process crashes.)
|
|
*/
|
|
_onCaptureOrTimeout(capture, reason) {
|
|
// Since timeouts start as an item is being processed, only the first
|
|
// item in the queue can be passed to this method.
|
|
if (capture !== this._captureQueue[0]) {
|
|
throw new Error("The capture should be at the head of the queue.");
|
|
}
|
|
|
|
this._captureQueue.shift();
|
|
this._capturesByURL.delete(capture.url);
|
|
if (reason != TEL_CAPTURE_DONE_OK) {
|
|
Services.obs.notifyObservers(null, "page-thumbnail:error", capture.url);
|
|
}
|
|
|
|
// Start the destroy-browser timer *before* processing the capture queue.
|
|
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
timer.initWithCallback(
|
|
this._destroyBrowser.bind(this),
|
|
this._destroyBrowserTimeout,
|
|
Ci.nsITimer.TYPE_ONE_SHOT
|
|
);
|
|
this._destroyBrowserTimer = timer;
|
|
|
|
this._processCaptureQueue();
|
|
},
|
|
|
|
_destroyBrowserTimeout: DESTROY_BROWSER_TIMEOUT,
|
|
};
|
|
|
|
BackgroundPageThumbs._init();
|
|
|
|
/**
|
|
* Represents a single capture request in the capture queue.
|
|
*
|
|
* @param url The URL to capture.
|
|
* @param captureCallback A function you want called when the capture
|
|
* completes.
|
|
* @param options The capture options.
|
|
*/
|
|
function Capture(url, captureCallback, options) {
|
|
this.url = url;
|
|
this.captureCallback = captureCallback;
|
|
this.redirectTimer = null;
|
|
this.timedOut = false;
|
|
this.options = options;
|
|
this.id = Capture.nextID++;
|
|
this.creationDate = new Date();
|
|
this.doneCallbacks = [];
|
|
if (options.onDone) {
|
|
this.doneCallbacks.push(options.onDone);
|
|
}
|
|
}
|
|
|
|
Capture.prototype = {
|
|
get pending() {
|
|
return !!this._timeoutTimer;
|
|
},
|
|
|
|
/**
|
|
* Sends a message to the content script to start the capture.
|
|
*
|
|
* @param browser The thumbnail browser.
|
|
*/
|
|
start(browser) {
|
|
this.startDate = new Date();
|
|
Glean.thumbnails.captureQueueTime.accumulateSingleSample(
|
|
this.startDate - this.creationDate
|
|
);
|
|
|
|
let fallbackTimeout = Cu.isInAutomation
|
|
? TESTING_CAPTURE_TIMEOUT
|
|
: DEFAULT_CAPTURE_TIMEOUT;
|
|
|
|
// timeout timer
|
|
let timeout =
|
|
typeof this.options.timeout == "number"
|
|
? this.options.timeout
|
|
: fallbackTimeout;
|
|
this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
this._timeoutTimer.initWithCallback(
|
|
this,
|
|
timeout,
|
|
Ci.nsITimer.TYPE_ONE_SHOT
|
|
);
|
|
|
|
this._browser = browser;
|
|
|
|
if (!browser.browsingContext) {
|
|
return;
|
|
}
|
|
|
|
this._pageLoadStartTime = new Date();
|
|
|
|
BackgroundPageThumbs._expectingAboutBlank = false;
|
|
|
|
let thumbnailsActor = browser.browsingContext.currentWindowGlobal.getActor(
|
|
"BackgroundThumbnails"
|
|
);
|
|
thumbnailsActor
|
|
.sendQuery("Browser:Thumbnail:LoadURL", {
|
|
url: this.url,
|
|
})
|
|
.then(
|
|
success => {
|
|
// If it failed, then this was likely a bad url. If successful,
|
|
// BackgroundPageThumbs.onStateChange will call _done() after the
|
|
// load has completed.
|
|
if (!success) {
|
|
this._done(browser, null, TEL_CAPTURE_DONE_BAD_URI);
|
|
}
|
|
},
|
|
() => {
|
|
// The query can fail when a crash occurs while loading. The error causes
|
|
// thumbnail crash tests to fail with an uninteresting error message.
|
|
}
|
|
);
|
|
},
|
|
|
|
readBlob: function readBlob(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
let reader = new FileReader();
|
|
reader.onloadend = function onloadend() {
|
|
if (reader.readyState != FileReader.DONE) {
|
|
reject(reader.error);
|
|
} else {
|
|
resolve(reader.result);
|
|
}
|
|
};
|
|
reader.readAsArrayBuffer(blob);
|
|
});
|
|
},
|
|
|
|
async pageLoaded(aBrowser) {
|
|
if (this.timedOut) {
|
|
this._done(aBrowser, null, TEL_CAPTURE_DONE_TIMEOUT);
|
|
return;
|
|
}
|
|
|
|
let waitTime = Cu.isInAutomation
|
|
? TESTING_SETTLE_WAIT_TIME
|
|
: SETTLE_WAIT_TIME;
|
|
|
|
// There was additional activity, so restart the wait timer
|
|
if (this.redirectTimer) {
|
|
this.redirectTimer.delay = waitTime;
|
|
return;
|
|
}
|
|
|
|
// The requested page has loaded or stopped/aborted, so capture the page
|
|
// soon but first let it settle in case of in-page redirects
|
|
await new Promise(resolve => {
|
|
this.redirectTimer = Cc["@mozilla.org/timer;1"].createInstance(
|
|
Ci.nsITimer
|
|
);
|
|
this.redirectTimer.init(resolve, waitTime, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
});
|
|
|
|
this.redirectTimer = null;
|
|
|
|
let pageLoadTime = new Date() - this._pageLoadStartTime;
|
|
let canvasDrawStartTime = new Date();
|
|
|
|
let canvas = PageThumbs.createCanvas(aBrowser.ownerGlobal, 1, 1);
|
|
try {
|
|
await PageThumbs.captureToCanvas(
|
|
aBrowser,
|
|
canvas,
|
|
{
|
|
isBackgroundThumb: true,
|
|
isImage: this.options.isImage,
|
|
backgroundColor: this.options.backgroundColor,
|
|
},
|
|
true
|
|
);
|
|
} catch (ex) {
|
|
this._done(
|
|
aBrowser,
|
|
null,
|
|
ex == "IMAGE_ZERO_DIMENSION"
|
|
? TEL_CAPTURE_DONE_IMAGE_ZERO_DIMENSION
|
|
: TEL_CAPTURE_DONE_LOAD_FAILED
|
|
);
|
|
return;
|
|
}
|
|
|
|
let canvasDrawTime = new Date() - canvasDrawStartTime;
|
|
|
|
let contentType =
|
|
(this.options.dontStore && this.options.contentType) ||
|
|
PageThumbs.contentType;
|
|
let imageData = await new Promise(resolve => {
|
|
canvas.toBlob(blob => {
|
|
resolve(blob, contentType);
|
|
}, contentType);
|
|
});
|
|
|
|
this._done(aBrowser, imageData, TEL_CAPTURE_DONE_OK, {
|
|
pageLoadTime,
|
|
canvasDrawTime,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* The only intended external use of this method is by the service when it's
|
|
* uninitializing and doing things like destroying the thumbnail browser. In
|
|
* that case the consumer's completion callback will never be called.
|
|
*/
|
|
destroy() {
|
|
// This method may be called for captures that haven't started yet, so
|
|
// guard against not yet having _timeoutTimer, _msgMan etc properties...
|
|
if (this._timeoutTimer) {
|
|
this._timeoutTimer.cancel();
|
|
delete this._timeoutTimer;
|
|
}
|
|
delete this.captureCallback;
|
|
delete this.doneCallbacks;
|
|
delete this.options;
|
|
},
|
|
|
|
// Called when the timeout timer fires.
|
|
notify() {
|
|
this.timedOut = true;
|
|
this._browser.stop();
|
|
},
|
|
|
|
_done(browser, imageData, reason, telemetry) {
|
|
// Note that _done will be called only once, by either receiveMessage or
|
|
// notify, since it calls destroy here, which cancels the timeout timer and
|
|
// removes the didCapture message listener.
|
|
let { captureCallback, doneCallbacks, options } = this;
|
|
this.destroy();
|
|
|
|
if (typeof reason != "number") {
|
|
throw new Error("A done reason must be given.");
|
|
}
|
|
|
|
Glean.thumbnails.captureDoneReason2.accumulateSingleSample(reason);
|
|
|
|
if (telemetry) {
|
|
// Telemetry is currently disabled in the content process (bug 680508).
|
|
Glean.thumbnails.capturePageLoadTime.accumulateSingleSample(
|
|
telemetry.pageLoadTime
|
|
);
|
|
Glean.thumbnails.captureCanvasDrawTime.accumulateSingleSample(
|
|
telemetry.canvasDrawTime
|
|
);
|
|
}
|
|
|
|
let done = (info = null) => {
|
|
captureCallback(this, reason);
|
|
for (let callback of doneCallbacks) {
|
|
try {
|
|
callback.call(options, this.url, reason, info);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
if (Services.prefs.getBoolPref(ABOUT_NEWTAB_SEGREGATION_PREF)) {
|
|
// Clear the data in the private container for thumbnails.
|
|
let privateIdentity = lazy.ContextualIdentityService.getPrivateIdentity(
|
|
"userContextIdInternal.thumbnail"
|
|
);
|
|
if (privateIdentity) {
|
|
Services.clearData.deleteDataFromOriginAttributesPattern({
|
|
userContextId: privateIdentity.userContextId,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
if (!imageData) {
|
|
done();
|
|
return;
|
|
}
|
|
|
|
this.readBlob(imageData).then(buffer => {
|
|
if (options.dontStore) {
|
|
done({
|
|
data: buffer,
|
|
originalUrl: this.url,
|
|
finalUrl: browser.currentURI.spec,
|
|
contentType: options.contentType || PageThumbs.contentType,
|
|
});
|
|
} else {
|
|
PageThumbs._store(this.url, browser.currentURI.spec, buffer, true).then(
|
|
done,
|
|
done
|
|
);
|
|
}
|
|
});
|
|
},
|
|
};
|
|
|
|
Capture.nextID = 0;
|
|
|
|
function schedule(callback) {
|
|
Services.tm.dispatchToMainThread(callback);
|
|
}
|