dom/browser-element/BrowserElementParent.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/browser-element/BrowserElementParent.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,855 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +let Cu = Components.utils;
    1.11 +let Ci = Components.interfaces;
    1.12 +let Cc = Components.classes;
    1.13 +let Cr = Components.results;
    1.14 +
    1.15 +/* BrowserElementParent injects script to listen for certain events in the
    1.16 + * child.  We then listen to messages from the child script and take
    1.17 + * appropriate action here in the parent.
    1.18 + */
    1.19 +
    1.20 +this.EXPORTED_SYMBOLS = ["BrowserElementParentBuilder"];
    1.21 +
    1.22 +Cu.import("resource://gre/modules/Services.jsm");
    1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.24 +Cu.import("resource://gre/modules/BrowserElementPromptService.jsm");
    1.25 +
    1.26 +XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function () {
    1.27 +  Cu.import("resource://gre/modules/Webapps.jsm");
    1.28 +  return DOMApplicationRegistry;
    1.29 +});
    1.30 +
    1.31 +const TOUCH_EVENTS_ENABLED_PREF = "dom.w3c_touch_events.enabled";
    1.32 +
    1.33 +function debug(msg) {
    1.34 +  //dump("BrowserElementParent.jsm - " + msg + "\n");
    1.35 +}
    1.36 +
    1.37 +function getIntPref(prefName, def) {
    1.38 +  try {
    1.39 +    return Services.prefs.getIntPref(prefName);
    1.40 +  }
    1.41 +  catch(err) {
    1.42 +    return def;
    1.43 +  }
    1.44 +}
    1.45 +
    1.46 +function exposeAll(obj) {
    1.47 +  // Filter for Objects and Arrays.
    1.48 +  if (typeof obj !== "object" || !obj)
    1.49 +    return;
    1.50 +
    1.51 +  // Recursively expose our children.
    1.52 +  Object.keys(obj).forEach(function(key) {
    1.53 +    exposeAll(obj[key]);
    1.54 +  });
    1.55 +
    1.56 +  // If we're not an Array, generate an __exposedProps__ object for ourselves.
    1.57 +  if (obj instanceof Array)
    1.58 +    return;
    1.59 +  var exposed = {};
    1.60 +  Object.keys(obj).forEach(function(key) {
    1.61 +    exposed[key] = 'rw';
    1.62 +  });
    1.63 +  obj.__exposedProps__ = exposed;
    1.64 +}
    1.65 +
    1.66 +function defineAndExpose(obj, name, value) {
    1.67 +  obj[name] = value;
    1.68 +  if (!('__exposedProps__' in obj))
    1.69 +    obj.__exposedProps__ = {};
    1.70 +  obj.__exposedProps__[name] = 'r';
    1.71 +}
    1.72 +
    1.73 +function visibilityChangeHandler(e) {
    1.74 +  // The visibilitychange event's target is the document.
    1.75 +  let win = e.target.defaultView;
    1.76 +
    1.77 +  if (!win._browserElementParents) {
    1.78 +    return;
    1.79 +  }
    1.80 +
    1.81 +  let beps = Cu.nondeterministicGetWeakMapKeys(win._browserElementParents);
    1.82 +  if (beps.length == 0) {
    1.83 +    win.removeEventListener('visibilitychange', visibilityChangeHandler);
    1.84 +    return;
    1.85 +  }
    1.86 +
    1.87 +  for (let i = 0; i < beps.length; i++) {
    1.88 +    beps[i]._ownerVisibilityChange();
    1.89 +  }
    1.90 +}
    1.91 +
    1.92 +this.BrowserElementParentBuilder = {
    1.93 +  create: function create(frameLoader, hasRemoteFrame, isPendingFrame) {
    1.94 +    return new BrowserElementParent(frameLoader, hasRemoteFrame);
    1.95 +  }
    1.96 +}
    1.97 +
    1.98 +
    1.99 +// The active input method iframe.
   1.100 +let activeInputFrame = null;
   1.101 +
   1.102 +function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) {
   1.103 +  debug("Creating new BrowserElementParent object for " + frameLoader);
   1.104 +  this._domRequestCounter = 0;
   1.105 +  this._pendingDOMRequests = {};
   1.106 +  this._hasRemoteFrame = hasRemoteFrame;
   1.107 +  this._nextPaintListeners = [];
   1.108 +
   1.109 +  this._frameLoader = frameLoader;
   1.110 +  this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement;
   1.111 +  let self = this;
   1.112 +  if (!this._frameElement) {
   1.113 +    debug("No frame element?");
   1.114 +    return;
   1.115 +  }
   1.116 +
   1.117 +  Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true);
   1.118 +  Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true);
   1.119 +
   1.120 +  let defineMethod = function(name, fn) {
   1.121 +    XPCNativeWrapper.unwrap(self._frameElement)[name] = function() {
   1.122 +      if (self._isAlive()) {
   1.123 +        return fn.apply(self, arguments);
   1.124 +      }
   1.125 +    };
   1.126 +  }
   1.127 +
   1.128 +  let defineNoReturnMethod = function(name, fn) {
   1.129 +    XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() {
   1.130 +      if (!self._mm) {
   1.131 +        // Remote browser haven't been created, we just queue the API call.
   1.132 +        let args = Array.slice(arguments);
   1.133 +        args.unshift(self);
   1.134 +        self._pendingAPICalls.push(method.bind.apply(fn, args));
   1.135 +        return;
   1.136 +      }
   1.137 +      if (self._isAlive()) {
   1.138 +        fn.apply(self, arguments);
   1.139 +      }
   1.140 +    };
   1.141 +  };
   1.142 +
   1.143 +  let defineDOMRequestMethod = function(domName, msgName) {
   1.144 +    XPCNativeWrapper.unwrap(self._frameElement)[domName] = function() {
   1.145 +      if (!self._mm) {
   1.146 +        return self._queueDOMRequest;
   1.147 +      }
   1.148 +      if (self._isAlive()) {
   1.149 +        return self._sendDOMRequest(msgName);
   1.150 +      }
   1.151 +    };
   1.152 +  }
   1.153 +
   1.154 +  // Define methods on the frame element.
   1.155 +  defineNoReturnMethod('setVisible', this._setVisible);
   1.156 +  defineDOMRequestMethod('getVisible', 'get-visible');
   1.157 +  defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent);
   1.158 +
   1.159 +  // 0 = disabled, 1 = enabled, 2 - auto detect
   1.160 +  if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) {
   1.161 +    defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent);
   1.162 +  }
   1.163 +  defineNoReturnMethod('goBack', this._goBack);
   1.164 +  defineNoReturnMethod('goForward', this._goForward);
   1.165 +  defineNoReturnMethod('reload', this._reload);
   1.166 +  defineNoReturnMethod('stop', this._stop);
   1.167 +  defineDOMRequestMethod('purgeHistory', 'purge-history');
   1.168 +  defineMethod('getScreenshot', this._getScreenshot);
   1.169 +  defineMethod('addNextPaintListener', this._addNextPaintListener);
   1.170 +  defineMethod('removeNextPaintListener', this._removeNextPaintListener);
   1.171 +  defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
   1.172 +  defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
   1.173 +
   1.174 +  let principal = this._frameElement.ownerDocument.nodePrincipal;
   1.175 +  let perm = Services.perms
   1.176 +             .testExactPermissionFromPrincipal(principal, "input-manage");
   1.177 +  if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) {
   1.178 +    defineMethod('setInputMethodActive', this._setInputMethodActive);
   1.179 +  }
   1.180 +
   1.181 +  // Listen to visibilitychange on the iframe's owner window, and forward
   1.182 +  // changes down to the child.  We want to do this while registering as few
   1.183 +  // visibilitychange listeners on _window as possible, because such a listener
   1.184 +  // may live longer than this BrowserElementParent object.
   1.185 +  //
   1.186 +  // To accomplish this, we register just one listener on the window, and have
   1.187 +  // it reference a WeakMap whose keys are all the BrowserElementParent objects
   1.188 +  // on the window.  Then when the listener fires, we iterate over the
   1.189 +  // WeakMap's keys (which we can do, because we're chrome) to notify the
   1.190 +  // BrowserElementParents.
   1.191 +  if (!this._window._browserElementParents) {
   1.192 +    this._window._browserElementParents = new WeakMap();
   1.193 +    this._window.addEventListener('visibilitychange',
   1.194 +                                  visibilityChangeHandler,
   1.195 +                                  /* useCapture = */ false,
   1.196 +                                  /* wantsUntrusted = */ false);
   1.197 +  }
   1.198 +
   1.199 +  this._window._browserElementParents.set(this, null);
   1.200 +
   1.201 +  // Insert ourself into the prompt service.
   1.202 +  BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this);
   1.203 +  if (!isPendingFrame) {
   1.204 +    this._setupMessageListener();
   1.205 +    this._registerAppManifest();
   1.206 +  } else {
   1.207 +    // if we are a pending frame, we setup message manager after
   1.208 +    // observing remote-browser-frame-shown
   1.209 +    this._pendingAPICalls = [];
   1.210 +    Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true);
   1.211 +  }
   1.212 +}
   1.213 +
   1.214 +BrowserElementParent.prototype = {
   1.215 +
   1.216 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   1.217 +                                         Ci.nsISupportsWeakReference]),
   1.218 +
   1.219 +  _runPendingAPICall: function() {
   1.220 +    if (!this._pendingAPICalls) {
   1.221 +      return;
   1.222 +    }
   1.223 +    for (let i = 0; i < this._pendingAPICalls.length; i++) {
   1.224 +      try {
   1.225 +        this._pendingAPICalls[i]();
   1.226 +      } catch (e) {
   1.227 +        // throw the expections from pending functions.
   1.228 +        debug('Exception when running pending API call: ' +  e);
   1.229 +      }
   1.230 +    }
   1.231 +    delete this._pendingAPICalls;
   1.232 +  },
   1.233 +
   1.234 +  _registerAppManifest: function() {
   1.235 +    // If this browser represents an app then let the Webapps module register for
   1.236 +    // any messages that it needs.
   1.237 +    let appManifestURL =
   1.238 +          this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL;
   1.239 +    if (appManifestURL) {
   1.240 +      let appId =
   1.241 +            DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL);
   1.242 +      if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) {
   1.243 +        DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId);
   1.244 +      }
   1.245 +    }
   1.246 +  },
   1.247 +
   1.248 +  _setupMessageListener: function() {
   1.249 +    this._mm = this._frameLoader.messageManager;
   1.250 +    let self = this;
   1.251 +
   1.252 +    // Messages we receive are handed to functions which take a (data) argument,
   1.253 +    // where |data| is the message manager's data object.
   1.254 +    // We use a single message and dispatch to various function based
   1.255 +    // on data.msg_name
   1.256 +    let mmCalls = {
   1.257 +      "hello": this._recvHello,
   1.258 +      "contextmenu": this._fireCtxMenuEvent,
   1.259 +      "locationchange": this._fireEventFromMsg,
   1.260 +      "loadstart": this._fireEventFromMsg,
   1.261 +      "loadend": this._fireEventFromMsg,
   1.262 +      "titlechange": this._fireEventFromMsg,
   1.263 +      "iconchange": this._fireEventFromMsg,
   1.264 +      "manifestchange": this._fireEventFromMsg,
   1.265 +      "metachange": this._fireEventFromMsg,
   1.266 +      "close": this._fireEventFromMsg,
   1.267 +      "resize": this._fireEventFromMsg,
   1.268 +      "activitydone": this._fireEventFromMsg,
   1.269 +      "opensearch": this._fireEventFromMsg,
   1.270 +      "securitychange": this._fireEventFromMsg,
   1.271 +      "error": this._fireEventFromMsg,
   1.272 +      "scroll": this._fireEventFromMsg,
   1.273 +      "firstpaint": this._fireEventFromMsg,
   1.274 +      "documentfirstpaint": this._fireEventFromMsg,
   1.275 +      "nextpaint": this._recvNextPaint,
   1.276 +      "keyevent": this._fireKeyEvent,
   1.277 +      "showmodalprompt": this._handleShowModalPrompt,
   1.278 +      "got-purge-history": this._gotDOMRequestResult,
   1.279 +      "got-screenshot": this._gotDOMRequestResult,
   1.280 +      "got-can-go-back": this._gotDOMRequestResult,
   1.281 +      "got-can-go-forward": this._gotDOMRequestResult,
   1.282 +      "fullscreen-origin-change": this._remoteFullscreenOriginChange,
   1.283 +      "rollback-fullscreen": this._remoteFrameFullscreenReverted,
   1.284 +      "exit-fullscreen": this._exitFullscreen,
   1.285 +      "got-visible": this._gotDOMRequestResult,
   1.286 +      "visibilitychange": this._childVisibilityChange,
   1.287 +      "got-set-input-method-active": this._gotDOMRequestResult
   1.288 +    };
   1.289 +
   1.290 +    this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
   1.291 +      if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) {
   1.292 +        return mmCalls[aMsg.data.msg_name].apply(self, arguments);
   1.293 +      }
   1.294 +    });
   1.295 +  },
   1.296 +
   1.297 +  /**
   1.298 +   * You shouldn't touch this._frameElement or this._window if _isAlive is
   1.299 +   * false.  (You'll likely get an exception if you do.)
   1.300 +   */
   1.301 +  _isAlive: function() {
   1.302 +    return !Cu.isDeadWrapper(this._frameElement) &&
   1.303 +           !Cu.isDeadWrapper(this._frameElement.ownerDocument) &&
   1.304 +           !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView);
   1.305 +  },
   1.306 +
   1.307 +  get _window() {
   1.308 +    return this._frameElement.ownerDocument.defaultView;
   1.309 +  },
   1.310 +
   1.311 +  get _windowUtils() {
   1.312 +    return this._window.QueryInterface(Ci.nsIInterfaceRequestor)
   1.313 +                       .getInterface(Ci.nsIDOMWindowUtils);
   1.314 +  },
   1.315 +
   1.316 +  promptAuth: function(authDetail, callback) {
   1.317 +    let evt;
   1.318 +    let self = this;
   1.319 +    let callbackCalled = false;
   1.320 +    let cancelCallback = function() {
   1.321 +      if (!callbackCalled) {
   1.322 +        callbackCalled = true;
   1.323 +        callback(false, null, null);
   1.324 +      }
   1.325 +    };
   1.326 +
   1.327 +    if (authDetail.isOnlyPassword) {
   1.328 +      // We don't handle password-only prompts, so just cancel it.
   1.329 +      cancelCallback();
   1.330 +      return;
   1.331 +    } else { /* username and password */
   1.332 +      let detail = {
   1.333 +        host:     authDetail.host,
   1.334 +        realm:    authDetail.realm
   1.335 +      };
   1.336 +
   1.337 +      evt = this._createEvent('usernameandpasswordrequired', detail,
   1.338 +                              /* cancelable */ true);
   1.339 +      defineAndExpose(evt.detail, 'authenticate', function(username, password) {
   1.340 +        if (callbackCalled)
   1.341 +          return;
   1.342 +        callbackCalled = true;
   1.343 +        callback(true, username, password);
   1.344 +      });
   1.345 +    }
   1.346 +
   1.347 +    defineAndExpose(evt.detail, 'cancel', function() {
   1.348 +      cancelCallback();
   1.349 +    });
   1.350 +
   1.351 +    this._frameElement.dispatchEvent(evt);
   1.352 +
   1.353 +    if (!evt.defaultPrevented) {
   1.354 +      cancelCallback();
   1.355 +    }
   1.356 +  },
   1.357 +
   1.358 +  _sendAsyncMsg: function(msg, data) {
   1.359 +    try {
   1.360 +      if (!data) {
   1.361 +        data = { };
   1.362 +      }
   1.363 +
   1.364 +      data.msg_name = msg;
   1.365 +      this._mm.sendAsyncMessage('browser-element-api:call', data);
   1.366 +    } catch (e) {
   1.367 +      return false;
   1.368 +    }
   1.369 +    return true;
   1.370 +  },
   1.371 +
   1.372 +  _recvHello: function() {
   1.373 +    debug("recvHello");
   1.374 +
   1.375 +    this._ready = true;
   1.376 +
   1.377 +    // Inform our child if our owner element's document is invisible.  Note
   1.378 +    // that we must do so here, rather than in the BrowserElementParent
   1.379 +    // constructor, because the BrowserElementChild may not be initialized when
   1.380 +    // we run our constructor.
   1.381 +    if (this._window.document.hidden) {
   1.382 +      this._ownerVisibilityChange();
   1.383 +    }
   1.384 +
   1.385 +    return {
   1.386 +      name: this._frameElement.getAttribute('name'),
   1.387 +      fullscreenAllowed:
   1.388 +        this._frameElement.hasAttribute('allowfullscreen') ||
   1.389 +        this._frameElement.hasAttribute('mozallowfullscreen')
   1.390 +    };
   1.391 +  },
   1.392 +
   1.393 +  _fireCtxMenuEvent: function(data) {
   1.394 +    let detail = data.json;
   1.395 +    let evtName = detail.msg_name;
   1.396 +
   1.397 +    debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail);
   1.398 +    let evt = this._createEvent(evtName, detail, /* cancellable */ true);
   1.399 +
   1.400 +    if (detail.contextmenu) {
   1.401 +      var self = this;
   1.402 +      defineAndExpose(evt.detail, 'contextMenuItemSelected', function(id) {
   1.403 +        self._sendAsyncMsg('fire-ctx-callback', {menuitem: id});
   1.404 +      });
   1.405 +    }
   1.406 +
   1.407 +    // The embedder may have default actions on context menu events, so
   1.408 +    // we fire a context menu event even if the child didn't define a
   1.409 +    // custom context menu
   1.410 +    return !this._frameElement.dispatchEvent(evt);
   1.411 +  },
   1.412 +
   1.413 +  /**
   1.414 +   * Fire either a vanilla or a custom event, depending on the contents of
   1.415 +   * |data|.
   1.416 +   */
   1.417 +  _fireEventFromMsg: function(data) {
   1.418 +    let detail = data.json;
   1.419 +    let name = detail.msg_name;
   1.420 +
   1.421 +    // For events that send a "_payload_" property, we just want to transmit
   1.422 +    // this in the event.
   1.423 +    if ("_payload_" in detail) {
   1.424 +      detail = detail._payload_;
   1.425 +    }
   1.426 +
   1.427 +    debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail));
   1.428 +    let evt = this._createEvent(name, detail,
   1.429 +                                /* cancelable = */ false);
   1.430 +    this._frameElement.dispatchEvent(evt);
   1.431 +  },
   1.432 +
   1.433 +  _handleShowModalPrompt: function(data) {
   1.434 +    // Fire a showmodalprmopt event on the iframe.  When this method is called,
   1.435 +    // the child is spinning in a nested event loop waiting for an
   1.436 +    // unblock-modal-prompt message.
   1.437 +    //
   1.438 +    // If the embedder calls preventDefault() on the showmodalprompt event,
   1.439 +    // we'll block the child until event.detail.unblock() is called.
   1.440 +    //
   1.441 +    // Otherwise, if preventDefault() is not called, we'll send the
   1.442 +    // unblock-modal-prompt message to the child as soon as the event is done
   1.443 +    // dispatching.
   1.444 +
   1.445 +    let detail = data.json;
   1.446 +    debug('handleShowPrompt ' + JSON.stringify(detail));
   1.447 +
   1.448 +    // Strip off the windowID property from the object we send along in the
   1.449 +    // event.
   1.450 +    let windowID = detail.windowID;
   1.451 +    delete detail.windowID;
   1.452 +    debug("Event will have detail: " + JSON.stringify(detail));
   1.453 +    let evt = this._createEvent('showmodalprompt', detail,
   1.454 +                                /* cancelable = */ true);
   1.455 +
   1.456 +    let self = this;
   1.457 +    let unblockMsgSent = false;
   1.458 +    function sendUnblockMsg() {
   1.459 +      if (unblockMsgSent) {
   1.460 +        return;
   1.461 +      }
   1.462 +      unblockMsgSent = true;
   1.463 +
   1.464 +      // We don't need to sanitize evt.detail.returnValue (e.g. converting the
   1.465 +      // return value of confirm() to a boolean); Gecko does that for us.
   1.466 +
   1.467 +      let data = { windowID: windowID,
   1.468 +                   returnValue: evt.detail.returnValue };
   1.469 +      self._sendAsyncMsg('unblock-modal-prompt', data);
   1.470 +    }
   1.471 +
   1.472 +    defineAndExpose(evt.detail, 'unblock', function() {
   1.473 +      sendUnblockMsg();
   1.474 +    });
   1.475 +
   1.476 +    this._frameElement.dispatchEvent(evt);
   1.477 +
   1.478 +    if (!evt.defaultPrevented) {
   1.479 +      // Unblock the inner frame immediately.  Otherwise we'll unblock upon
   1.480 +      // evt.detail.unblock().
   1.481 +      sendUnblockMsg();
   1.482 +    }
   1.483 +  },
   1.484 +
   1.485 +  _createEvent: function(evtName, detail, cancelable) {
   1.486 +    // This will have to change if we ever want to send a CustomEvent with null
   1.487 +    // detail.  For now, it's OK.
   1.488 +    if (detail !== undefined && detail !== null) {
   1.489 +      exposeAll(detail);
   1.490 +      return new this._window.CustomEvent('mozbrowser' + evtName,
   1.491 +                                          { bubbles: true,
   1.492 +                                            cancelable: cancelable,
   1.493 +                                            detail: detail });
   1.494 +    }
   1.495 +
   1.496 +    return new this._window.Event('mozbrowser' + evtName,
   1.497 +                                  { bubbles: true,
   1.498 +                                    cancelable: cancelable });
   1.499 +  },
   1.500 +
   1.501 +  /**
   1.502 +   * If remote frame haven't been set up, we enqueue a function that get a
   1.503 +   * DOMRequest until the remote frame is ready and return another DOMRequest
   1.504 +   * to caller. When we get the real DOMRequest, we will help forward the
   1.505 +   * success/error callback to the DOMRequest that caller got.
   1.506 +   */
   1.507 +  _queueDOMRequest: function(msgName, args) {
   1.508 +    if (!this._pendingAPICalls) {
   1.509 +      return;
   1.510 +    }
   1.511 +
   1.512 +    let req = Services.DOMRequest.createRequest(this._window);
   1.513 +    let self = this;
   1.514 +    let getRealDOMRequest = function() {
   1.515 +      let realReq = self._sendDOMRequest(msgName, args);
   1.516 +      realReq.onsuccess = function(v) {
   1.517 +        Services.DOMRequest.fireSuccess(req, v);
   1.518 +      };
   1.519 +      realReq.onerror = function(v) {
   1.520 +        Services.DOMRequest.fireError(req, v);
   1.521 +      };
   1.522 +    };
   1.523 +    this._pendingAPICalls.push(getRealDOMRequest);
   1.524 +    return req;
   1.525 +  },
   1.526 +
   1.527 +  /**
   1.528 +   * Kick off a DOMRequest in the child process.
   1.529 +   *
   1.530 +   * We'll fire an event called |msgName| on the child process, passing along
   1.531 +   * an object with two fields:
   1.532 +   *
   1.533 +   *  - id:  the ID of this request.
   1.534 +   *  - arg: arguments to pass to the child along with this request.
   1.535 +   *
   1.536 +   * We expect the child to pass the ID back to us upon completion of the
   1.537 +   * request.  See _gotDOMRequestResult.
   1.538 +   */
   1.539 +  _sendDOMRequest: function(msgName, args) {
   1.540 +    let id = 'req_' + this._domRequestCounter++;
   1.541 +    let req = Services.DOMRequest.createRequest(this._window);
   1.542 +    if (this._sendAsyncMsg(msgName, {id: id, args: args})) {
   1.543 +      this._pendingDOMRequests[id] = req;
   1.544 +    } else {
   1.545 +      Services.DOMRequest.fireErrorAsync(req, "fail");
   1.546 +    }
   1.547 +    return req;
   1.548 +  },
   1.549 +
   1.550 +  /**
   1.551 +   * Called when the child process finishes handling a DOMRequest.  data.json
   1.552 +   * must have the fields [id, successRv], if the DOMRequest was successful, or
   1.553 +   * [id, errorMsg], if the request was not successful.
   1.554 +   *
   1.555 +   * The fields have the following meanings:
   1.556 +   *
   1.557 +   *  - id:        the ID of the DOM request (see _sendDOMRequest)
   1.558 +   *  - successRv: the request's return value, if the request succeeded
   1.559 +   *  - errorMsg:  the message to pass to DOMRequest.fireError(), if the request
   1.560 +   *               failed.
   1.561 +   *
   1.562 +   */
   1.563 +  _gotDOMRequestResult: function(data) {
   1.564 +    let req = this._pendingDOMRequests[data.json.id];
   1.565 +    delete this._pendingDOMRequests[data.json.id];
   1.566 +
   1.567 +    if ('successRv' in data.json) {
   1.568 +      debug("Successful gotDOMRequestResult.");
   1.569 +      Services.DOMRequest.fireSuccess(req, data.json.successRv);
   1.570 +    }
   1.571 +    else {
   1.572 +      debug("Got error in gotDOMRequestResult.");
   1.573 +      Services.DOMRequest.fireErrorAsync(req, data.json.errorMsg);
   1.574 +    }
   1.575 +  },
   1.576 +
   1.577 +  _setVisible: function(visible) {
   1.578 +    this._sendAsyncMsg('set-visible', {visible: visible});
   1.579 +    this._frameLoader.visible = visible;
   1.580 +  },
   1.581 +
   1.582 +  _sendMouseEvent: function(type, x, y, button, clickCount, modifiers) {
   1.583 +    this._sendAsyncMsg("send-mouse-event", {
   1.584 +      "type": type,
   1.585 +      "x": x,
   1.586 +      "y": y,
   1.587 +      "button": button,
   1.588 +      "clickCount": clickCount,
   1.589 +      "modifiers": modifiers
   1.590 +    });
   1.591 +  },
   1.592 +
   1.593 +  _sendTouchEvent: function(type, identifiers, touchesX, touchesY,
   1.594 +                            radiisX, radiisY, rotationAngles, forces,
   1.595 +                            count, modifiers) {
   1.596 +
   1.597 +    let tabParent = this._frameLoader.tabParent;
   1.598 +    if (tabParent && tabParent.useAsyncPanZoom) {
   1.599 +      tabParent.injectTouchEvent(type,
   1.600 +                                 identifiers,
   1.601 +                                 touchesX,
   1.602 +                                 touchesY,
   1.603 +                                 radiisX,
   1.604 +                                 radiisY,
   1.605 +                                 rotationAngles,
   1.606 +                                 forces,
   1.607 +                                 count,
   1.608 +                                 modifiers);
   1.609 +    } else {
   1.610 +      this._sendAsyncMsg("send-touch-event", {
   1.611 +        "type": type,
   1.612 +        "identifiers": identifiers,
   1.613 +        "touchesX": touchesX,
   1.614 +        "touchesY": touchesY,
   1.615 +        "radiisX": radiisX,
   1.616 +        "radiisY": radiisY,
   1.617 +        "rotationAngles": rotationAngles,
   1.618 +        "forces": forces,
   1.619 +        "count": count,
   1.620 +        "modifiers": modifiers
   1.621 +      });
   1.622 +    }
   1.623 +  },
   1.624 +
   1.625 +  _goBack: function() {
   1.626 +    this._sendAsyncMsg('go-back');
   1.627 +  },
   1.628 +
   1.629 +  _goForward: function() {
   1.630 +    this._sendAsyncMsg('go-forward');
   1.631 +  },
   1.632 +
   1.633 +  _reload: function(hardReload) {
   1.634 +    this._sendAsyncMsg('reload', {hardReload: hardReload});
   1.635 +  },
   1.636 +
   1.637 +  _stop: function() {
   1.638 +    this._sendAsyncMsg('stop');
   1.639 +  },
   1.640 +
   1.641 +  _getScreenshot: function(_width, _height, _mimeType) {
   1.642 +    let width = parseInt(_width);
   1.643 +    let height = parseInt(_height);
   1.644 +    let mimeType = (typeof _mimeType === 'string') ?
   1.645 +      _mimeType.trim().toLowerCase() : 'image/jpeg';
   1.646 +    if (isNaN(width) || isNaN(height) || width < 0 || height < 0) {
   1.647 +      throw Components.Exception("Invalid argument",
   1.648 +                                 Cr.NS_ERROR_INVALID_ARG);
   1.649 +    }
   1.650 +
   1.651 +    if (!this._mm) {
   1.652 +      // Child haven't been loaded.
   1.653 +      return this._queueDOMRequest('get-screenshot',
   1.654 +                                   {width: width, height: height,
   1.655 +                                    mimeType: mimeType});
   1.656 +    }
   1.657 +
   1.658 +    return this._sendDOMRequest('get-screenshot',
   1.659 +                                {width: width, height: height,
   1.660 +                                 mimeType: mimeType});
   1.661 +  },
   1.662 +
   1.663 +  _recvNextPaint: function(data) {
   1.664 +    let listeners = this._nextPaintListeners;
   1.665 +    this._nextPaintListeners = [];
   1.666 +    for (let listener of listeners) {
   1.667 +      try {
   1.668 +        listener();
   1.669 +      } catch (e) {
   1.670 +        // If a listener throws we'll continue.
   1.671 +      }
   1.672 +    }
   1.673 +  },
   1.674 +
   1.675 +  _addNextPaintListener: function(listener) {
   1.676 +    if (typeof listener != 'function')
   1.677 +      throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
   1.678 +
   1.679 +    let self = this;
   1.680 +    let run = function() {
   1.681 +      if (self._nextPaintListeners.push(listener) == 1)
   1.682 +        self._sendAsyncMsg('activate-next-paint-listener');
   1.683 +    };
   1.684 +    if (!this._mm) {
   1.685 +      this._pendingAPICalls.push(run);
   1.686 +    } else {
   1.687 +      run();
   1.688 +    }
   1.689 +  },
   1.690 +
   1.691 +  _removeNextPaintListener: function(listener) {
   1.692 +    if (typeof listener != 'function')
   1.693 +      throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
   1.694 +
   1.695 +    let self = this;
   1.696 +    let run = function() {
   1.697 +      for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) {
   1.698 +        if (self._nextPaintListeners[i] == listener) {
   1.699 +          self._nextPaintListeners.splice(i, 1);
   1.700 +          break;
   1.701 +        }
   1.702 +      }
   1.703 +
   1.704 +      if (self._nextPaintListeners.length == 0)
   1.705 +        self._sendAsyncMsg('deactivate-next-paint-listener');
   1.706 +    };
   1.707 +    if (!this._mm) {
   1.708 +      this._pendingAPICalls.push(run);
   1.709 +    } else {
   1.710 +      run();
   1.711 +    }
   1.712 +  },
   1.713 +
   1.714 +  _setInputMethodActive: function(isActive) {
   1.715 +    if (typeof isActive !== 'boolean') {
   1.716 +      throw Components.Exception("Invalid argument",
   1.717 +                                 Cr.NS_ERROR_INVALID_ARG);
   1.718 +    }
   1.719 +
   1.720 +    let req = Services.DOMRequest.createRequest(this._window);
   1.721 +
   1.722 +    // Deactivate the old input method if needed.
   1.723 +    if (activeInputFrame && isActive) {
   1.724 +      if (Cu.isDeadWrapper(activeInputFrame)) {
   1.725 +        // If the activeInputFrame is already a dead object,
   1.726 +        // we should simply set it to null directly.
   1.727 +        activeInputFrame = null;
   1.728 +        this._sendSetInputMethodActiveDOMRequest(req, isActive);
   1.729 +      } else {
   1.730 +        let reqOld = XPCNativeWrapper.unwrap(activeInputFrame)
   1.731 +                                     .setInputMethodActive(false);
   1.732 +
   1.733 +        // We wan't to continue regardless whether this req succeeded
   1.734 +        reqOld.onsuccess = reqOld.onerror = function() {
   1.735 +          let setActive = function() {
   1.736 +            activeInputFrame = null;
   1.737 +            this._sendSetInputMethodActiveDOMRequest(req, isActive);
   1.738 +          }.bind(this);
   1.739 +
   1.740 +          if (this._ready) {
   1.741 +            setActive();
   1.742 +            return;
   1.743 +          }
   1.744 +
   1.745 +          // Wait for the hello event from BrowserElementChild
   1.746 +          let onReady = function(aMsg) {
   1.747 +            if (this._isAlive() && (aMsg.data.msg_name === 'hello')) {
   1.748 +              setActive();
   1.749 +
   1.750 +              this._mm.removeMessageListener('browser-element-api:call',
   1.751 +                onReady);
   1.752 +            }
   1.753 +          }.bind(this);
   1.754 +
   1.755 +          this._mm.addMessageListener('browser-element-api:call', onReady);
   1.756 +        }.bind(this);
   1.757 +      }
   1.758 +    } else {
   1.759 +      this._sendSetInputMethodActiveDOMRequest(req, isActive);
   1.760 +    }
   1.761 +    return req;
   1.762 +  },
   1.763 +
   1.764 +  _sendSetInputMethodActiveDOMRequest: function(req, isActive) {
   1.765 +    let id = 'req_' + this._domRequestCounter++;
   1.766 +    let data = {
   1.767 +      id : id,
   1.768 +      args: { isActive: isActive }
   1.769 +    };
   1.770 +    if (this._sendAsyncMsg('set-input-method-active', data)) {
   1.771 +      activeInputFrame = this._frameElement;
   1.772 +      this._pendingDOMRequests[id] = req;
   1.773 +    } else {
   1.774 +      Services.DOMRequest.fireErrorAsync(req, 'fail');
   1.775 +    }
   1.776 +  },
   1.777 +
   1.778 +  _fireKeyEvent: function(data) {
   1.779 +    let evt = this._window.document.createEvent("KeyboardEvent");
   1.780 +    evt.initKeyEvent(data.json.type, true, true, this._window,
   1.781 +                     false, false, false, false, // modifiers
   1.782 +                     data.json.keyCode,
   1.783 +                     data.json.charCode);
   1.784 +
   1.785 +    this._frameElement.dispatchEvent(evt);
   1.786 +  },
   1.787 +
   1.788 +  /**
   1.789 +   * Called when the visibility of the window which owns this iframe changes.
   1.790 +   */
   1.791 +  _ownerVisibilityChange: function() {
   1.792 +    this._sendAsyncMsg('owner-visibility-change',
   1.793 +                       {visible: !this._window.document.hidden});
   1.794 +  },
   1.795 +
   1.796 +  /*
   1.797 +   * Called when the child notices that its visibility has changed.
   1.798 +   *
   1.799 +   * This is sometimes redundant; for example, the child's visibility may
   1.800 +   * change in response to a setVisible request that we made here!  But it's
   1.801 +   * not always redundant; for example, the child's visibility may change in
   1.802 +   * response to its parent docshell being hidden.
   1.803 +   */
   1.804 +  _childVisibilityChange: function(data) {
   1.805 +    debug("_childVisibilityChange(" + data.json.visible + ")");
   1.806 +    this._frameLoader.visible = data.json.visible;
   1.807 +
   1.808 +    this._fireEventFromMsg(data);
   1.809 +  },
   1.810 +
   1.811 +  _exitFullscreen: function() {
   1.812 +    this._windowUtils.exitFullscreen();
   1.813 +  },
   1.814 +
   1.815 +  _remoteFullscreenOriginChange: function(data) {
   1.816 +    let origin = data.json._payload_;
   1.817 +    this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin);
   1.818 +  },
   1.819 +
   1.820 +  _remoteFrameFullscreenReverted: function(data) {
   1.821 +    this._windowUtils.remoteFrameFullscreenReverted();
   1.822 +  },
   1.823 +
   1.824 +  _fireFatalError: function() {
   1.825 +    let evt = this._createEvent('error', {type: 'fatal'},
   1.826 +                                /* cancelable = */ false);
   1.827 +    this._frameElement.dispatchEvent(evt);
   1.828 +  },
   1.829 +
   1.830 +  observe: function(subject, topic, data) {
   1.831 +    switch(topic) {
   1.832 +    case 'oop-frameloader-crashed':
   1.833 +      if (this._isAlive() && subject == this._frameLoader) {
   1.834 +        this._fireFatalError();
   1.835 +      }
   1.836 +      break;
   1.837 +    case 'ask-children-to-exit-fullscreen':
   1.838 +      if (this._isAlive() &&
   1.839 +          this._frameElement.ownerDocument == subject &&
   1.840 +          this._hasRemoteFrame) {
   1.841 +        this._sendAsyncMsg('exit-fullscreen');
   1.842 +      }
   1.843 +      break;
   1.844 +    case 'remote-browser-frame-shown':
   1.845 +      if (this._frameLoader == subject) {
   1.846 +        if (!this._mm) {
   1.847 +          this._setupMessageListener();
   1.848 +          this._registerAppManifest();
   1.849 +          this._runPendingAPICall();
   1.850 +        }
   1.851 +        Services.obs.removeObserver(this, 'remote-browser-frame-shown');
   1.852 +      }
   1.853 +    default:
   1.854 +      debug('Unknown topic: ' + topic);
   1.855 +      break;
   1.856 +    };
   1.857 +  },
   1.858 +};

mercurial