dom/browser-element/BrowserElementParent.jsm

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 let Cu = Components.utils;
michael@0 8 let Ci = Components.interfaces;
michael@0 9 let Cc = Components.classes;
michael@0 10 let Cr = Components.results;
michael@0 11
michael@0 12 /* BrowserElementParent injects script to listen for certain events in the
michael@0 13 * child. We then listen to messages from the child script and take
michael@0 14 * appropriate action here in the parent.
michael@0 15 */
michael@0 16
michael@0 17 this.EXPORTED_SYMBOLS = ["BrowserElementParentBuilder"];
michael@0 18
michael@0 19 Cu.import("resource://gre/modules/Services.jsm");
michael@0 20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 21 Cu.import("resource://gre/modules/BrowserElementPromptService.jsm");
michael@0 22
michael@0 23 XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function () {
michael@0 24 Cu.import("resource://gre/modules/Webapps.jsm");
michael@0 25 return DOMApplicationRegistry;
michael@0 26 });
michael@0 27
michael@0 28 const TOUCH_EVENTS_ENABLED_PREF = "dom.w3c_touch_events.enabled";
michael@0 29
michael@0 30 function debug(msg) {
michael@0 31 //dump("BrowserElementParent.jsm - " + msg + "\n");
michael@0 32 }
michael@0 33
michael@0 34 function getIntPref(prefName, def) {
michael@0 35 try {
michael@0 36 return Services.prefs.getIntPref(prefName);
michael@0 37 }
michael@0 38 catch(err) {
michael@0 39 return def;
michael@0 40 }
michael@0 41 }
michael@0 42
michael@0 43 function exposeAll(obj) {
michael@0 44 // Filter for Objects and Arrays.
michael@0 45 if (typeof obj !== "object" || !obj)
michael@0 46 return;
michael@0 47
michael@0 48 // Recursively expose our children.
michael@0 49 Object.keys(obj).forEach(function(key) {
michael@0 50 exposeAll(obj[key]);
michael@0 51 });
michael@0 52
michael@0 53 // If we're not an Array, generate an __exposedProps__ object for ourselves.
michael@0 54 if (obj instanceof Array)
michael@0 55 return;
michael@0 56 var exposed = {};
michael@0 57 Object.keys(obj).forEach(function(key) {
michael@0 58 exposed[key] = 'rw';
michael@0 59 });
michael@0 60 obj.__exposedProps__ = exposed;
michael@0 61 }
michael@0 62
michael@0 63 function defineAndExpose(obj, name, value) {
michael@0 64 obj[name] = value;
michael@0 65 if (!('__exposedProps__' in obj))
michael@0 66 obj.__exposedProps__ = {};
michael@0 67 obj.__exposedProps__[name] = 'r';
michael@0 68 }
michael@0 69
michael@0 70 function visibilityChangeHandler(e) {
michael@0 71 // The visibilitychange event's target is the document.
michael@0 72 let win = e.target.defaultView;
michael@0 73
michael@0 74 if (!win._browserElementParents) {
michael@0 75 return;
michael@0 76 }
michael@0 77
michael@0 78 let beps = Cu.nondeterministicGetWeakMapKeys(win._browserElementParents);
michael@0 79 if (beps.length == 0) {
michael@0 80 win.removeEventListener('visibilitychange', visibilityChangeHandler);
michael@0 81 return;
michael@0 82 }
michael@0 83
michael@0 84 for (let i = 0; i < beps.length; i++) {
michael@0 85 beps[i]._ownerVisibilityChange();
michael@0 86 }
michael@0 87 }
michael@0 88
michael@0 89 this.BrowserElementParentBuilder = {
michael@0 90 create: function create(frameLoader, hasRemoteFrame, isPendingFrame) {
michael@0 91 return new BrowserElementParent(frameLoader, hasRemoteFrame);
michael@0 92 }
michael@0 93 }
michael@0 94
michael@0 95
michael@0 96 // The active input method iframe.
michael@0 97 let activeInputFrame = null;
michael@0 98
michael@0 99 function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) {
michael@0 100 debug("Creating new BrowserElementParent object for " + frameLoader);
michael@0 101 this._domRequestCounter = 0;
michael@0 102 this._pendingDOMRequests = {};
michael@0 103 this._hasRemoteFrame = hasRemoteFrame;
michael@0 104 this._nextPaintListeners = [];
michael@0 105
michael@0 106 this._frameLoader = frameLoader;
michael@0 107 this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
michael@0 108 let self = this;
michael@0 109 if (!this._frameElement) {
michael@0 110 debug("No frame element?");
michael@0 111 return;
michael@0 112 }
michael@0 113
michael@0 114 Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true);
michael@0 115 Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true);
michael@0 116
michael@0 117 let defineMethod = function(name, fn) {
michael@0 118 XPCNativeWrapper.unwrap(self._frameElement)[name] = function() {
michael@0 119 if (self._isAlive()) {
michael@0 120 return fn.apply(self, arguments);
michael@0 121 }
michael@0 122 };
michael@0 123 }
michael@0 124
michael@0 125 let defineNoReturnMethod = function(name, fn) {
michael@0 126 XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() {
michael@0 127 if (!self._mm) {
michael@0 128 // Remote browser haven't been created, we just queue the API call.
michael@0 129 let args = Array.slice(arguments);
michael@0 130 args.unshift(self);
michael@0 131 self._pendingAPICalls.push(method.bind.apply(fn, args));
michael@0 132 return;
michael@0 133 }
michael@0 134 if (self._isAlive()) {
michael@0 135 fn.apply(self, arguments);
michael@0 136 }
michael@0 137 };
michael@0 138 };
michael@0 139
michael@0 140 let defineDOMRequestMethod = function(domName, msgName) {
michael@0 141 XPCNativeWrapper.unwrap(self._frameElement)[domName] = function() {
michael@0 142 if (!self._mm) {
michael@0 143 return self._queueDOMRequest;
michael@0 144 }
michael@0 145 if (self._isAlive()) {
michael@0 146 return self._sendDOMRequest(msgName);
michael@0 147 }
michael@0 148 };
michael@0 149 }
michael@0 150
michael@0 151 // Define methods on the frame element.
michael@0 152 defineNoReturnMethod('setVisible', this._setVisible);
michael@0 153 defineDOMRequestMethod('getVisible', 'get-visible');
michael@0 154 defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent);
michael@0 155
michael@0 156 // 0 = disabled, 1 = enabled, 2 - auto detect
michael@0 157 if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) {
michael@0 158 defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent);
michael@0 159 }
michael@0 160 defineNoReturnMethod('goBack', this._goBack);
michael@0 161 defineNoReturnMethod('goForward', this._goForward);
michael@0 162 defineNoReturnMethod('reload', this._reload);
michael@0 163 defineNoReturnMethod('stop', this._stop);
michael@0 164 defineDOMRequestMethod('purgeHistory', 'purge-history');
michael@0 165 defineMethod('getScreenshot', this._getScreenshot);
michael@0 166 defineMethod('addNextPaintListener', this._addNextPaintListener);
michael@0 167 defineMethod('removeNextPaintListener', this._removeNextPaintListener);
michael@0 168 defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
michael@0 169 defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
michael@0 170
michael@0 171 let principal = this._frameElement.ownerDocument.nodePrincipal;
michael@0 172 let perm = Services.perms
michael@0 173 .testExactPermissionFromPrincipal(principal, "input-manage");
michael@0 174 if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) {
michael@0 175 defineMethod('setInputMethodActive', this._setInputMethodActive);
michael@0 176 }
michael@0 177
michael@0 178 // Listen to visibilitychange on the iframe's owner window, and forward
michael@0 179 // changes down to the child. We want to do this while registering as few
michael@0 180 // visibilitychange listeners on _window as possible, because such a listener
michael@0 181 // may live longer than this BrowserElementParent object.
michael@0 182 //
michael@0 183 // To accomplish this, we register just one listener on the window, and have
michael@0 184 // it reference a WeakMap whose keys are all the BrowserElementParent objects
michael@0 185 // on the window. Then when the listener fires, we iterate over the
michael@0 186 // WeakMap's keys (which we can do, because we're chrome) to notify the
michael@0 187 // BrowserElementParents.
michael@0 188 if (!this._window._browserElementParents) {
michael@0 189 this._window._browserElementParents = new WeakMap();
michael@0 190 this._window.addEventListener('visibilitychange',
michael@0 191 visibilityChangeHandler,
michael@0 192 /* useCapture = */ false,
michael@0 193 /* wantsUntrusted = */ false);
michael@0 194 }
michael@0 195
michael@0 196 this._window._browserElementParents.set(this, null);
michael@0 197
michael@0 198 // Insert ourself into the prompt service.
michael@0 199 BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this);
michael@0 200 if (!isPendingFrame) {
michael@0 201 this._setupMessageListener();
michael@0 202 this._registerAppManifest();
michael@0 203 } else {
michael@0 204 // if we are a pending frame, we setup message manager after
michael@0 205 // observing remote-browser-frame-shown
michael@0 206 this._pendingAPICalls = [];
michael@0 207 Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true);
michael@0 208 }
michael@0 209 }
michael@0 210
michael@0 211 BrowserElementParent.prototype = {
michael@0 212
michael@0 213 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
michael@0 214 Ci.nsISupportsWeakReference]),
michael@0 215
michael@0 216 _runPendingAPICall: function() {
michael@0 217 if (!this._pendingAPICalls) {
michael@0 218 return;
michael@0 219 }
michael@0 220 for (let i = 0; i < this._pendingAPICalls.length; i++) {
michael@0 221 try {
michael@0 222 this._pendingAPICalls[i]();
michael@0 223 } catch (e) {
michael@0 224 // throw the expections from pending functions.
michael@0 225 debug('Exception when running pending API call: ' + e);
michael@0 226 }
michael@0 227 }
michael@0 228 delete this._pendingAPICalls;
michael@0 229 },
michael@0 230
michael@0 231 _registerAppManifest: function() {
michael@0 232 // If this browser represents an app then let the Webapps module register for
michael@0 233 // any messages that it needs.
michael@0 234 let appManifestURL =
michael@0 235 this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL;
michael@0 236 if (appManifestURL) {
michael@0 237 let appId =
michael@0 238 DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL);
michael@0 239 if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) {
michael@0 240 DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId);
michael@0 241 }
michael@0 242 }
michael@0 243 },
michael@0 244
michael@0 245 _setupMessageListener: function() {
michael@0 246 this._mm = this._frameLoader.messageManager;
michael@0 247 let self = this;
michael@0 248
michael@0 249 // Messages we receive are handed to functions which take a (data) argument,
michael@0 250 // where |data| is the message manager's data object.
michael@0 251 // We use a single message and dispatch to various function based
michael@0 252 // on data.msg_name
michael@0 253 let mmCalls = {
michael@0 254 "hello": this._recvHello,
michael@0 255 "contextmenu": this._fireCtxMenuEvent,
michael@0 256 "locationchange": this._fireEventFromMsg,
michael@0 257 "loadstart": this._fireEventFromMsg,
michael@0 258 "loadend": this._fireEventFromMsg,
michael@0 259 "titlechange": this._fireEventFromMsg,
michael@0 260 "iconchange": this._fireEventFromMsg,
michael@0 261 "manifestchange": this._fireEventFromMsg,
michael@0 262 "metachange": this._fireEventFromMsg,
michael@0 263 "close": this._fireEventFromMsg,
michael@0 264 "resize": this._fireEventFromMsg,
michael@0 265 "activitydone": this._fireEventFromMsg,
michael@0 266 "opensearch": this._fireEventFromMsg,
michael@0 267 "securitychange": this._fireEventFromMsg,
michael@0 268 "error": this._fireEventFromMsg,
michael@0 269 "scroll": this._fireEventFromMsg,
michael@0 270 "firstpaint": this._fireEventFromMsg,
michael@0 271 "documentfirstpaint": this._fireEventFromMsg,
michael@0 272 "nextpaint": this._recvNextPaint,
michael@0 273 "keyevent": this._fireKeyEvent,
michael@0 274 "showmodalprompt": this._handleShowModalPrompt,
michael@0 275 "got-purge-history": this._gotDOMRequestResult,
michael@0 276 "got-screenshot": this._gotDOMRequestResult,
michael@0 277 "got-can-go-back": this._gotDOMRequestResult,
michael@0 278 "got-can-go-forward": this._gotDOMRequestResult,
michael@0 279 "fullscreen-origin-change": this._remoteFullscreenOriginChange,
michael@0 280 "rollback-fullscreen": this._remoteFrameFullscreenReverted,
michael@0 281 "exit-fullscreen": this._exitFullscreen,
michael@0 282 "got-visible": this._gotDOMRequestResult,
michael@0 283 "visibilitychange": this._childVisibilityChange,
michael@0 284 "got-set-input-method-active": this._gotDOMRequestResult
michael@0 285 };
michael@0 286
michael@0 287 this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
michael@0 288 if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) {
michael@0 289 return mmCalls[aMsg.data.msg_name].apply(self, arguments);
michael@0 290 }
michael@0 291 });
michael@0 292 },
michael@0 293
michael@0 294 /**
michael@0 295 * You shouldn't touch this._frameElement or this._window if _isAlive is
michael@0 296 * false. (You'll likely get an exception if you do.)
michael@0 297 */
michael@0 298 _isAlive: function() {
michael@0 299 return !Cu.isDeadWrapper(this._frameElement) &&
michael@0 300 !Cu.isDeadWrapper(this._frameElement.ownerDocument) &&
michael@0 301 !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView);
michael@0 302 },
michael@0 303
michael@0 304 get _window() {
michael@0 305 return this._frameElement.ownerDocument.defaultView;
michael@0 306 },
michael@0 307
michael@0 308 get _windowUtils() {
michael@0 309 return this._window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 310 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 311 },
michael@0 312
michael@0 313 promptAuth: function(authDetail, callback) {
michael@0 314 let evt;
michael@0 315 let self = this;
michael@0 316 let callbackCalled = false;
michael@0 317 let cancelCallback = function() {
michael@0 318 if (!callbackCalled) {
michael@0 319 callbackCalled = true;
michael@0 320 callback(false, null, null);
michael@0 321 }
michael@0 322 };
michael@0 323
michael@0 324 if (authDetail.isOnlyPassword) {
michael@0 325 // We don't handle password-only prompts, so just cancel it.
michael@0 326 cancelCallback();
michael@0 327 return;
michael@0 328 } else { /* username and password */
michael@0 329 let detail = {
michael@0 330 host: authDetail.host,
michael@0 331 realm: authDetail.realm
michael@0 332 };
michael@0 333
michael@0 334 evt = this._createEvent('usernameandpasswordrequired', detail,
michael@0 335 /* cancelable */ true);
michael@0 336 defineAndExpose(evt.detail, 'authenticate', function(username, password) {
michael@0 337 if (callbackCalled)
michael@0 338 return;
michael@0 339 callbackCalled = true;
michael@0 340 callback(true, username, password);
michael@0 341 });
michael@0 342 }
michael@0 343
michael@0 344 defineAndExpose(evt.detail, 'cancel', function() {
michael@0 345 cancelCallback();
michael@0 346 });
michael@0 347
michael@0 348 this._frameElement.dispatchEvent(evt);
michael@0 349
michael@0 350 if (!evt.defaultPrevented) {
michael@0 351 cancelCallback();
michael@0 352 }
michael@0 353 },
michael@0 354
michael@0 355 _sendAsyncMsg: function(msg, data) {
michael@0 356 try {
michael@0 357 if (!data) {
michael@0 358 data = { };
michael@0 359 }
michael@0 360
michael@0 361 data.msg_name = msg;
michael@0 362 this._mm.sendAsyncMessage('browser-element-api:call', data);
michael@0 363 } catch (e) {
michael@0 364 return false;
michael@0 365 }
michael@0 366 return true;
michael@0 367 },
michael@0 368
michael@0 369 _recvHello: function() {
michael@0 370 debug("recvHello");
michael@0 371
michael@0 372 this._ready = true;
michael@0 373
michael@0 374 // Inform our child if our owner element's document is invisible. Note
michael@0 375 // that we must do so here, rather than in the BrowserElementParent
michael@0 376 // constructor, because the BrowserElementChild may not be initialized when
michael@0 377 // we run our constructor.
michael@0 378 if (this._window.document.hidden) {
michael@0 379 this._ownerVisibilityChange();
michael@0 380 }
michael@0 381
michael@0 382 return {
michael@0 383 name: this._frameElement.getAttribute('name'),
michael@0 384 fullscreenAllowed:
michael@0 385 this._frameElement.hasAttribute('allowfullscreen') ||
michael@0 386 this._frameElement.hasAttribute('mozallowfullscreen')
michael@0 387 };
michael@0 388 },
michael@0 389
michael@0 390 _fireCtxMenuEvent: function(data) {
michael@0 391 let detail = data.json;
michael@0 392 let evtName = detail.msg_name;
michael@0 393
michael@0 394 debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail);
michael@0 395 let evt = this._createEvent(evtName, detail, /* cancellable */ true);
michael@0 396
michael@0 397 if (detail.contextmenu) {
michael@0 398 var self = this;
michael@0 399 defineAndExpose(evt.detail, 'contextMenuItemSelected', function(id) {
michael@0 400 self._sendAsyncMsg('fire-ctx-callback', {menuitem: id});
michael@0 401 });
michael@0 402 }
michael@0 403
michael@0 404 // The embedder may have default actions on context menu events, so
michael@0 405 // we fire a context menu event even if the child didn't define a
michael@0 406 // custom context menu
michael@0 407 return !this._frameElement.dispatchEvent(evt);
michael@0 408 },
michael@0 409
michael@0 410 /**
michael@0 411 * Fire either a vanilla or a custom event, depending on the contents of
michael@0 412 * |data|.
michael@0 413 */
michael@0 414 _fireEventFromMsg: function(data) {
michael@0 415 let detail = data.json;
michael@0 416 let name = detail.msg_name;
michael@0 417
michael@0 418 // For events that send a "_payload_" property, we just want to transmit
michael@0 419 // this in the event.
michael@0 420 if ("_payload_" in detail) {
michael@0 421 detail = detail._payload_;
michael@0 422 }
michael@0 423
michael@0 424 debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail));
michael@0 425 let evt = this._createEvent(name, detail,
michael@0 426 /* cancelable = */ false);
michael@0 427 this._frameElement.dispatchEvent(evt);
michael@0 428 },
michael@0 429
michael@0 430 _handleShowModalPrompt: function(data) {
michael@0 431 // Fire a showmodalprmopt event on the iframe. When this method is called,
michael@0 432 // the child is spinning in a nested event loop waiting for an
michael@0 433 // unblock-modal-prompt message.
michael@0 434 //
michael@0 435 // If the embedder calls preventDefault() on the showmodalprompt event,
michael@0 436 // we'll block the child until event.detail.unblock() is called.
michael@0 437 //
michael@0 438 // Otherwise, if preventDefault() is not called, we'll send the
michael@0 439 // unblock-modal-prompt message to the child as soon as the event is done
michael@0 440 // dispatching.
michael@0 441
michael@0 442 let detail = data.json;
michael@0 443 debug('handleShowPrompt ' + JSON.stringify(detail));
michael@0 444
michael@0 445 // Strip off the windowID property from the object we send along in the
michael@0 446 // event.
michael@0 447 let windowID = detail.windowID;
michael@0 448 delete detail.windowID;
michael@0 449 debug("Event will have detail: " + JSON.stringify(detail));
michael@0 450 let evt = this._createEvent('showmodalprompt', detail,
michael@0 451 /* cancelable = */ true);
michael@0 452
michael@0 453 let self = this;
michael@0 454 let unblockMsgSent = false;
michael@0 455 function sendUnblockMsg() {
michael@0 456 if (unblockMsgSent) {
michael@0 457 return;
michael@0 458 }
michael@0 459 unblockMsgSent = true;
michael@0 460
michael@0 461 // We don't need to sanitize evt.detail.returnValue (e.g. converting the
michael@0 462 // return value of confirm() to a boolean); Gecko does that for us.
michael@0 463
michael@0 464 let data = { windowID: windowID,
michael@0 465 returnValue: evt.detail.returnValue };
michael@0 466 self._sendAsyncMsg('unblock-modal-prompt', data);
michael@0 467 }
michael@0 468
michael@0 469 defineAndExpose(evt.detail, 'unblock', function() {
michael@0 470 sendUnblockMsg();
michael@0 471 });
michael@0 472
michael@0 473 this._frameElement.dispatchEvent(evt);
michael@0 474
michael@0 475 if (!evt.defaultPrevented) {
michael@0 476 // Unblock the inner frame immediately. Otherwise we'll unblock upon
michael@0 477 // evt.detail.unblock().
michael@0 478 sendUnblockMsg();
michael@0 479 }
michael@0 480 },
michael@0 481
michael@0 482 _createEvent: function(evtName, detail, cancelable) {
michael@0 483 // This will have to change if we ever want to send a CustomEvent with null
michael@0 484 // detail. For now, it's OK.
michael@0 485 if (detail !== undefined && detail !== null) {
michael@0 486 exposeAll(detail);
michael@0 487 return new this._window.CustomEvent('mozbrowser' + evtName,
michael@0 488 { bubbles: true,
michael@0 489 cancelable: cancelable,
michael@0 490 detail: detail });
michael@0 491 }
michael@0 492
michael@0 493 return new this._window.Event('mozbrowser' + evtName,
michael@0 494 { bubbles: true,
michael@0 495 cancelable: cancelable });
michael@0 496 },
michael@0 497
michael@0 498 /**
michael@0 499 * If remote frame haven't been set up, we enqueue a function that get a
michael@0 500 * DOMRequest until the remote frame is ready and return another DOMRequest
michael@0 501 * to caller. When we get the real DOMRequest, we will help forward the
michael@0 502 * success/error callback to the DOMRequest that caller got.
michael@0 503 */
michael@0 504 _queueDOMRequest: function(msgName, args) {
michael@0 505 if (!this._pendingAPICalls) {
michael@0 506 return;
michael@0 507 }
michael@0 508
michael@0 509 let req = Services.DOMRequest.createRequest(this._window);
michael@0 510 let self = this;
michael@0 511 let getRealDOMRequest = function() {
michael@0 512 let realReq = self._sendDOMRequest(msgName, args);
michael@0 513 realReq.onsuccess = function(v) {
michael@0 514 Services.DOMRequest.fireSuccess(req, v);
michael@0 515 };
michael@0 516 realReq.onerror = function(v) {
michael@0 517 Services.DOMRequest.fireError(req, v);
michael@0 518 };
michael@0 519 };
michael@0 520 this._pendingAPICalls.push(getRealDOMRequest);
michael@0 521 return req;
michael@0 522 },
michael@0 523
michael@0 524 /**
michael@0 525 * Kick off a DOMRequest in the child process.
michael@0 526 *
michael@0 527 * We'll fire an event called |msgName| on the child process, passing along
michael@0 528 * an object with two fields:
michael@0 529 *
michael@0 530 * - id: the ID of this request.
michael@0 531 * - arg: arguments to pass to the child along with this request.
michael@0 532 *
michael@0 533 * We expect the child to pass the ID back to us upon completion of the
michael@0 534 * request. See _gotDOMRequestResult.
michael@0 535 */
michael@0 536 _sendDOMRequest: function(msgName, args) {
michael@0 537 let id = 'req_' + this._domRequestCounter++;
michael@0 538 let req = Services.DOMRequest.createRequest(this._window);
michael@0 539 if (this._sendAsyncMsg(msgName, {id: id, args: args})) {
michael@0 540 this._pendingDOMRequests[id] = req;
michael@0 541 } else {
michael@0 542 Services.DOMRequest.fireErrorAsync(req, "fail");
michael@0 543 }
michael@0 544 return req;
michael@0 545 },
michael@0 546
michael@0 547 /**
michael@0 548 * Called when the child process finishes handling a DOMRequest. data.json
michael@0 549 * must have the fields [id, successRv], if the DOMRequest was successful, or
michael@0 550 * [id, errorMsg], if the request was not successful.
michael@0 551 *
michael@0 552 * The fields have the following meanings:
michael@0 553 *
michael@0 554 * - id: the ID of the DOM request (see _sendDOMRequest)
michael@0 555 * - successRv: the request's return value, if the request succeeded
michael@0 556 * - errorMsg: the message to pass to DOMRequest.fireError(), if the request
michael@0 557 * failed.
michael@0 558 *
michael@0 559 */
michael@0 560 _gotDOMRequestResult: function(data) {
michael@0 561 let req = this._pendingDOMRequests[data.json.id];
michael@0 562 delete this._pendingDOMRequests[data.json.id];
michael@0 563
michael@0 564 if ('successRv' in data.json) {
michael@0 565 debug("Successful gotDOMRequestResult.");
michael@0 566 Services.DOMRequest.fireSuccess(req, data.json.successRv);
michael@0 567 }
michael@0 568 else {
michael@0 569 debug("Got error in gotDOMRequestResult.");
michael@0 570 Services.DOMRequest.fireErrorAsync(req, data.json.errorMsg);
michael@0 571 }
michael@0 572 },
michael@0 573
michael@0 574 _setVisible: function(visible) {
michael@0 575 this._sendAsyncMsg('set-visible', {visible: visible});
michael@0 576 this._frameLoader.visible = visible;
michael@0 577 },
michael@0 578
michael@0 579 _sendMouseEvent: function(type, x, y, button, clickCount, modifiers) {
michael@0 580 this._sendAsyncMsg("send-mouse-event", {
michael@0 581 "type": type,
michael@0 582 "x": x,
michael@0 583 "y": y,
michael@0 584 "button": button,
michael@0 585 "clickCount": clickCount,
michael@0 586 "modifiers": modifiers
michael@0 587 });
michael@0 588 },
michael@0 589
michael@0 590 _sendTouchEvent: function(type, identifiers, touchesX, touchesY,
michael@0 591 radiisX, radiisY, rotationAngles, forces,
michael@0 592 count, modifiers) {
michael@0 593
michael@0 594 let tabParent = this._frameLoader.tabParent;
michael@0 595 if (tabParent && tabParent.useAsyncPanZoom) {
michael@0 596 tabParent.injectTouchEvent(type,
michael@0 597 identifiers,
michael@0 598 touchesX,
michael@0 599 touchesY,
michael@0 600 radiisX,
michael@0 601 radiisY,
michael@0 602 rotationAngles,
michael@0 603 forces,
michael@0 604 count,
michael@0 605 modifiers);
michael@0 606 } else {
michael@0 607 this._sendAsyncMsg("send-touch-event", {
michael@0 608 "type": type,
michael@0 609 "identifiers": identifiers,
michael@0 610 "touchesX": touchesX,
michael@0 611 "touchesY": touchesY,
michael@0 612 "radiisX": radiisX,
michael@0 613 "radiisY": radiisY,
michael@0 614 "rotationAngles": rotationAngles,
michael@0 615 "forces": forces,
michael@0 616 "count": count,
michael@0 617 "modifiers": modifiers
michael@0 618 });
michael@0 619 }
michael@0 620 },
michael@0 621
michael@0 622 _goBack: function() {
michael@0 623 this._sendAsyncMsg('go-back');
michael@0 624 },
michael@0 625
michael@0 626 _goForward: function() {
michael@0 627 this._sendAsyncMsg('go-forward');
michael@0 628 },
michael@0 629
michael@0 630 _reload: function(hardReload) {
michael@0 631 this._sendAsyncMsg('reload', {hardReload: hardReload});
michael@0 632 },
michael@0 633
michael@0 634 _stop: function() {
michael@0 635 this._sendAsyncMsg('stop');
michael@0 636 },
michael@0 637
michael@0 638 _getScreenshot: function(_width, _height, _mimeType) {
michael@0 639 let width = parseInt(_width);
michael@0 640 let height = parseInt(_height);
michael@0 641 let mimeType = (typeof _mimeType === 'string') ?
michael@0 642 _mimeType.trim().toLowerCase() : 'image/jpeg';
michael@0 643 if (isNaN(width) || isNaN(height) || width < 0 || height < 0) {
michael@0 644 throw Components.Exception("Invalid argument",
michael@0 645 Cr.NS_ERROR_INVALID_ARG);
michael@0 646 }
michael@0 647
michael@0 648 if (!this._mm) {
michael@0 649 // Child haven't been loaded.
michael@0 650 return this._queueDOMRequest('get-screenshot',
michael@0 651 {width: width, height: height,
michael@0 652 mimeType: mimeType});
michael@0 653 }
michael@0 654
michael@0 655 return this._sendDOMRequest('get-screenshot',
michael@0 656 {width: width, height: height,
michael@0 657 mimeType: mimeType});
michael@0 658 },
michael@0 659
michael@0 660 _recvNextPaint: function(data) {
michael@0 661 let listeners = this._nextPaintListeners;
michael@0 662 this._nextPaintListeners = [];
michael@0 663 for (let listener of listeners) {
michael@0 664 try {
michael@0 665 listener();
michael@0 666 } catch (e) {
michael@0 667 // If a listener throws we'll continue.
michael@0 668 }
michael@0 669 }
michael@0 670 },
michael@0 671
michael@0 672 _addNextPaintListener: function(listener) {
michael@0 673 if (typeof listener != 'function')
michael@0 674 throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
michael@0 675
michael@0 676 let self = this;
michael@0 677 let run = function() {
michael@0 678 if (self._nextPaintListeners.push(listener) == 1)
michael@0 679 self._sendAsyncMsg('activate-next-paint-listener');
michael@0 680 };
michael@0 681 if (!this._mm) {
michael@0 682 this._pendingAPICalls.push(run);
michael@0 683 } else {
michael@0 684 run();
michael@0 685 }
michael@0 686 },
michael@0 687
michael@0 688 _removeNextPaintListener: function(listener) {
michael@0 689 if (typeof listener != 'function')
michael@0 690 throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
michael@0 691
michael@0 692 let self = this;
michael@0 693 let run = function() {
michael@0 694 for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) {
michael@0 695 if (self._nextPaintListeners[i] == listener) {
michael@0 696 self._nextPaintListeners.splice(i, 1);
michael@0 697 break;
michael@0 698 }
michael@0 699 }
michael@0 700
michael@0 701 if (self._nextPaintListeners.length == 0)
michael@0 702 self._sendAsyncMsg('deactivate-next-paint-listener');
michael@0 703 };
michael@0 704 if (!this._mm) {
michael@0 705 this._pendingAPICalls.push(run);
michael@0 706 } else {
michael@0 707 run();
michael@0 708 }
michael@0 709 },
michael@0 710
michael@0 711 _setInputMethodActive: function(isActive) {
michael@0 712 if (typeof isActive !== 'boolean') {
michael@0 713 throw Components.Exception("Invalid argument",
michael@0 714 Cr.NS_ERROR_INVALID_ARG);
michael@0 715 }
michael@0 716
michael@0 717 let req = Services.DOMRequest.createRequest(this._window);
michael@0 718
michael@0 719 // Deactivate the old input method if needed.
michael@0 720 if (activeInputFrame && isActive) {
michael@0 721 if (Cu.isDeadWrapper(activeInputFrame)) {
michael@0 722 // If the activeInputFrame is already a dead object,
michael@0 723 // we should simply set it to null directly.
michael@0 724 activeInputFrame = null;
michael@0 725 this._sendSetInputMethodActiveDOMRequest(req, isActive);
michael@0 726 } else {
michael@0 727 let reqOld = XPCNativeWrapper.unwrap(activeInputFrame)
michael@0 728 .setInputMethodActive(false);
michael@0 729
michael@0 730 // We wan't to continue regardless whether this req succeeded
michael@0 731 reqOld.onsuccess = reqOld.onerror = function() {
michael@0 732 let setActive = function() {
michael@0 733 activeInputFrame = null;
michael@0 734 this._sendSetInputMethodActiveDOMRequest(req, isActive);
michael@0 735 }.bind(this);
michael@0 736
michael@0 737 if (this._ready) {
michael@0 738 setActive();
michael@0 739 return;
michael@0 740 }
michael@0 741
michael@0 742 // Wait for the hello event from BrowserElementChild
michael@0 743 let onReady = function(aMsg) {
michael@0 744 if (this._isAlive() && (aMsg.data.msg_name === 'hello')) {
michael@0 745 setActive();
michael@0 746
michael@0 747 this._mm.removeMessageListener('browser-element-api:call',
michael@0 748 onReady);
michael@0 749 }
michael@0 750 }.bind(this);
michael@0 751
michael@0 752 this._mm.addMessageListener('browser-element-api:call', onReady);
michael@0 753 }.bind(this);
michael@0 754 }
michael@0 755 } else {
michael@0 756 this._sendSetInputMethodActiveDOMRequest(req, isActive);
michael@0 757 }
michael@0 758 return req;
michael@0 759 },
michael@0 760
michael@0 761 _sendSetInputMethodActiveDOMRequest: function(req, isActive) {
michael@0 762 let id = 'req_' + this._domRequestCounter++;
michael@0 763 let data = {
michael@0 764 id : id,
michael@0 765 args: { isActive: isActive }
michael@0 766 };
michael@0 767 if (this._sendAsyncMsg('set-input-method-active', data)) {
michael@0 768 activeInputFrame = this._frameElement;
michael@0 769 this._pendingDOMRequests[id] = req;
michael@0 770 } else {
michael@0 771 Services.DOMRequest.fireErrorAsync(req, 'fail');
michael@0 772 }
michael@0 773 },
michael@0 774
michael@0 775 _fireKeyEvent: function(data) {
michael@0 776 let evt = this._window.document.createEvent("KeyboardEvent");
michael@0 777 evt.initKeyEvent(data.json.type, true, true, this._window,
michael@0 778 false, false, false, false, // modifiers
michael@0 779 data.json.keyCode,
michael@0 780 data.json.charCode);
michael@0 781
michael@0 782 this._frameElement.dispatchEvent(evt);
michael@0 783 },
michael@0 784
michael@0 785 /**
michael@0 786 * Called when the visibility of the window which owns this iframe changes.
michael@0 787 */
michael@0 788 _ownerVisibilityChange: function() {
michael@0 789 this._sendAsyncMsg('owner-visibility-change',
michael@0 790 {visible: !this._window.document.hidden});
michael@0 791 },
michael@0 792
michael@0 793 /*
michael@0 794 * Called when the child notices that its visibility has changed.
michael@0 795 *
michael@0 796 * This is sometimes redundant; for example, the child's visibility may
michael@0 797 * change in response to a setVisible request that we made here! But it's
michael@0 798 * not always redundant; for example, the child's visibility may change in
michael@0 799 * response to its parent docshell being hidden.
michael@0 800 */
michael@0 801 _childVisibilityChange: function(data) {
michael@0 802 debug("_childVisibilityChange(" + data.json.visible + ")");
michael@0 803 this._frameLoader.visible = data.json.visible;
michael@0 804
michael@0 805 this._fireEventFromMsg(data);
michael@0 806 },
michael@0 807
michael@0 808 _exitFullscreen: function() {
michael@0 809 this._windowUtils.exitFullscreen();
michael@0 810 },
michael@0 811
michael@0 812 _remoteFullscreenOriginChange: function(data) {
michael@0 813 let origin = data.json._payload_;
michael@0 814 this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin);
michael@0 815 },
michael@0 816
michael@0 817 _remoteFrameFullscreenReverted: function(data) {
michael@0 818 this._windowUtils.remoteFrameFullscreenReverted();
michael@0 819 },
michael@0 820
michael@0 821 _fireFatalError: function() {
michael@0 822 let evt = this._createEvent('error', {type: 'fatal'},
michael@0 823 /* cancelable = */ false);
michael@0 824 this._frameElement.dispatchEvent(evt);
michael@0 825 },
michael@0 826
michael@0 827 observe: function(subject, topic, data) {
michael@0 828 switch(topic) {
michael@0 829 case 'oop-frameloader-crashed':
michael@0 830 if (this._isAlive() && subject == this._frameLoader) {
michael@0 831 this._fireFatalError();
michael@0 832 }
michael@0 833 break;
michael@0 834 case 'ask-children-to-exit-fullscreen':
michael@0 835 if (this._isAlive() &&
michael@0 836 this._frameElement.ownerDocument == subject &&
michael@0 837 this._hasRemoteFrame) {
michael@0 838 this._sendAsyncMsg('exit-fullscreen');
michael@0 839 }
michael@0 840 break;
michael@0 841 case 'remote-browser-frame-shown':
michael@0 842 if (this._frameLoader == subject) {
michael@0 843 if (!this._mm) {
michael@0 844 this._setupMessageListener();
michael@0 845 this._registerAppManifest();
michael@0 846 this._runPendingAPICall();
michael@0 847 }
michael@0 848 Services.obs.removeObserver(this, 'remote-browser-frame-shown');
michael@0 849 }
michael@0 850 default:
michael@0 851 debug('Unknown topic: ' + topic);
michael@0 852 break;
michael@0 853 };
michael@0 854 },
michael@0 855 };

mercurial