Bug 1110030 - part7 - Interface between HardwareKeyHandler and Input Method App. r=masayuki, r=smaug
authorchunminchang <cchang@mozilla.com>
Mon, 21 Mar 2016 14:06:04 +0800
changeset 328838 2ba85d06b69b916f1e65c946a022b617b74ffb27
parent 328837 ebbd79395164168a51c850dc80c7daf156dfa99b
child 328839 f74a43d0b9eb6b7a3ae5e03c508eab14ac01620e
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki, smaug
bugs1110030
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1110030 - part7 - Interface between HardwareKeyHandler and Input Method App. r=masayuki, r=smaug
dom/inputmethod/Keyboard.jsm
dom/inputmethod/MozKeyboard.js
dom/inputmethod/moz.build
dom/webidl/InputMethod.webidl
--- a/dom/inputmethod/Keyboard.jsm
+++ b/dom/inputmethod/Keyboard.jsm
@@ -18,16 +18,25 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
                                   "resource://gre/modules/SystemAppProxy.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "appsService", function() {
   return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
 });
 
+XPCOMUtils.defineLazyGetter(this, "hardwareKeyHandler", function() {
+#ifdef MOZ_B2G
+  return Cc["@mozilla.org/HardwareKeyHandler;1"]
+         .getService(Ci.nsIHardwareKeyHandler);
+#else
+  return null;
+#endif
+});
+
 var Utils = {
   getMMFromMessage: function u_getMMFromMessage(msg) {
     let mm;
     try {
       mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
                      .frameLoader.messageManager;
     } catch(e) {
       mm = msg.target;
@@ -36,16 +45,28 @@ var Utils = {
     return mm;
   },
   checkPermissionForMM: function u_checkPermissionForMM(mm, permName) {
     return mm.assertPermission(permName);
   }
 };
 
 this.Keyboard = {
+#ifdef MOZ_B2G
+  // For receving keyboard event fired from hardware before it's dispatched,
+  // |this| object is used to be the listener to get the forwarded event.
+  // As the listener, |this| object must implement nsIHardwareKeyEventListener
+  // and nsSupportsWeakReference.
+  // Please see nsIHardwareKeyHandler.idl to get more information.
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIHardwareKeyEventListener,
+    Ci.nsISupportsWeakReference
+  ]),
+#endif
+  _isConnectedToHardwareKeyHandler: false,
   _formMM: null,      // The current web page message manager.
   _keyboardMM: null,  // The keyboard app message manager.
   _keyboardID: -1,    // The keyboard app's ID number. -1 = invalid
   _nextKeyboardID: 0, // The ID number counter.
   _systemMMs: [],     // The message managers registered to handle system async
                       // messages.
   _supportsSwitchingTypes: [],
   _systemMessageNames: [
@@ -54,17 +75,18 @@ this.Keyboard = {
   ],
 
   _messageNames: [
     'RemoveFocus',
     'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
     'SwitchToNextInputMethod', 'HideInputMethod',
     'SendKey', 'GetContext',
     'SetComposition', 'EndComposition',
-    'RegisterSync', 'Unregister'
+    'RegisterSync', 'Unregister',
+    'ReplyHardwareKeyEvent'
   ],
 
   get formMM() {
     if (this._formMM && !Cu.isDeadWrapper(this._formMM))
       return this._formMM;
 
     return null;
   },
@@ -83,17 +105,20 @@ this.Keyboard = {
     try {
       this.formMM.sendAsyncMessage(name, data);
     } catch(e) { }
   },
 
   sendToKeyboard: function(name, data) {
     try {
       this._keyboardMM.sendAsyncMessage(name, data);
-    } catch(e) { }
+    } catch(e) {
+      return false;
+    }
+    return true;
   },
 
   sendToSystem: function(name, data) {
     if (!this._systemMMs.length) {
       dump("Keyboard.jsm: Attempt to send message " + name +
         " to system but no message manager registered.\n");
 
       return;
@@ -106,27 +131,42 @@ this.Keyboard = {
   },
 
   init: function keyboardInit() {
     Services.obs.addObserver(this, 'inprocess-browser-shown', false);
     Services.obs.addObserver(this, 'remote-browser-shown', false);
     Services.obs.addObserver(this, 'oop-frameloader-crashed', false);
     Services.obs.addObserver(this, 'message-manager-close', false);
 
+    // For receiving the native hardware keyboard event
+    if (hardwareKeyHandler) {
+      hardwareKeyHandler.registerListener(this);
+    }
+
     for (let name of this._messageNames) {
       ppmm.addMessageListener('Keyboard:' + name, this);
     }
 
     for (let name of this._systemMessageNames) {
       ppmm.addMessageListener('System:' + name, this);
     }
 
     this.inputRegistryGlue = new InputRegistryGlue();
   },
 
+  // This method will be registered into nsIHardwareKeyHandler:
+  // Send the initialized dictionary retrieved from the native keyboard event
+  // to input-method-app for generating a new event.
+  onHardwareKey: function onHardwareKeyReceived(evt) {
+    return this.sendToKeyboard('Keyboard:ReceiveHardwareKeyEvent', {
+      type: evt.type,
+      keyDict: evt.initDict
+    });
+  },
+
   observe: function keyboardObserve(subject, topic, data) {
     let frameLoader = null;
     let mm = null;
 
     if (topic == 'message-manager-close') {
       mm = subject;
     } else {
       frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
@@ -310,25 +350,38 @@ this.Keyboard = {
           // and we want to return the id back to inputmethod
           return this._keyboardID;
         }
         break;
       case 'Keyboard:Unregister':
         this._keyboardMM = null;
         this._keyboardID = -1;
         break;
+      case 'Keyboard:ReplyHardwareKeyEvent':
+        if (hardwareKeyHandler) {
+          let reply = msg.data;
+          hardwareKeyHandler.onHandledByInputMethodApp(reply.type,
+                                                       reply.defaultPrevented);
+        }
+        break;
     }
   },
 
   handleFocus: function keyboardHandleFocus(msg) {
     // Set the formMM to the new message manager received.
     let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
                 .frameLoader.messageManager;
     this.formMM = mm;
 
+    // Notify the nsIHardwareKeyHandler that the input-method-app is active now.
+    if (hardwareKeyHandler && !this._isConnectedToHardwareKeyHandler) {
+      this._isConnectedToHardwareKeyHandler = true;
+      hardwareKeyHandler.onInputMethodAppConnected();
+    }
+
     // Notify the current active input app to gain focus.
     this.forwardEvent('Keyboard:Focus', msg);
 
     // Notify System app, used also to render value selectors for now;
     // that's why we need the info about choices / min / max here as well...
     this.sendToSystem('System:Focus', msg.data);
 
     // XXX: To be removed when content migrate away from mozChromeEvents.
@@ -351,16 +404,23 @@ this.Keyboard = {
     // ipc messages from two processes.
     if (mm !== this.formMM) {
       return;
     }
 
     // unset formMM
     this.formMM = null;
 
+    // Notify the nsIHardwareKeyHandler that
+    // the input-method-app is disabled now.
+    if (hardwareKeyHandler && this._isConnectedToHardwareKeyHandler) {
+      this._isConnectedToHardwareKeyHandler = false;
+      hardwareKeyHandler.onInputMethodAppDisconnected();
+    }
+
     this.forwardEvent('Keyboard:Blur', msg);
     this.sendToSystem('System:Blur', {});
 
     // XXX: To be removed when content migrate away from mozChromeEvents.
     SystemAppProxy.dispatchEvent({
       type: 'inputmethod-contextchange',
       inputType: 'blur'
     });
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -426,16 +426,17 @@ MozInputMethod.prototype = {
 
     Services.obs.addObserver(this, "inner-window-destroyed", false);
 
     cpmm.addWeakMessageListener('Keyboard:Focus', this);
     cpmm.addWeakMessageListener('Keyboard:Blur', this);
     cpmm.addWeakMessageListener('Keyboard:SelectionChange', this);
     cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this);
     cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
+    cpmm.addWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this);
     cpmm.addWeakMessageListener('InputRegistry:Result:OK', this);
     cpmm.addWeakMessageListener('InputRegistry:Result:Error', this);
 
     if (this._hasInputManagePerm(win)) {
       this._inputManageId = cpmm.sendSyncMessage('System:RegisterSync', {})[0];
       cpmm.addWeakMessageListener('System:Focus', this);
       cpmm.addWeakMessageListener('System:Blur', this);
       cpmm.addWeakMessageListener('System:ShowAll', this);
@@ -450,16 +451,17 @@ MozInputMethod.prototype = {
     this._mgmt = null;
     this._wrappedMgmt = null;
 
     cpmm.removeWeakMessageListener('Keyboard:Focus', this);
     cpmm.removeWeakMessageListener('Keyboard:Blur', this);
     cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this);
     cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this);
     cpmm.removeWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
+    cpmm.removeWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this);
     cpmm.removeWeakMessageListener('InputRegistry:Result:OK', this);
     cpmm.removeWeakMessageListener('InputRegistry:Result:Error', this);
     this.setActive(false);
 
     if (typeof this._inputManageId === 'number') {
       cpmm.sendAsyncMessage('System:Unregister', {
         'id': this._inputManageId
       });
@@ -503,17 +505,34 @@ MozInputMethod.prototype = {
         }
         break;
       case 'Keyboard:GetContext:Result:OK':
         this.setInputContext(data);
         break;
       case 'Keyboard:SupportsSwitchingTypesChange':
         this._supportsSwitchingTypes = data.types;
         break;
+      case 'Keyboard:ReceiveHardwareKeyEvent':
+        if (!Ci.nsIHardwareKeyHandler) {
+          break;
+        }
 
+        let defaultPrevented = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
+
+        // |event.preventDefault()| is allowed to be called only when
+        // |event.cancelable| is true
+        if (this._inputcontext && data.keyDict.cancelable) {
+          defaultPrevented |= this._inputcontext.forwardHardwareKeyEvent(data);
+        }
+
+        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ReplyHardwareKeyEvent', {
+                                       type: data.type,
+                                       defaultPrevented: defaultPrevented
+                                     });
+        break;
       case 'InputRegistry:Result:OK':
         resolver.resolve();
 
         break;
 
       case 'InputRegistry:Result:Error':
         resolver.reject(data.error);
 
@@ -679,17 +698,17 @@ MozInputMethod.prototype = {
   _hasInputManagePerm: function(win) {
     let principal = win.document.nodePrincipal;
     let perm = Services.perms.testExactPermissionFromPrincipal(principal,
                                                                "input-manage");
     return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
   }
 };
 
- /**
+/**
  * ==============================================
  * InputContextDOMRequestIpcHelper
  * ==============================================
  */
 function InputContextDOMRequestIpcHelper(win) {
   this.initDOMRequestHelper(win,
     ["Keyboard:GetText:Result:OK",
      "Keyboard:GetText:Result:Error",
@@ -778,17 +797,30 @@ MozInputContextSurroundingTextChangeEven
     return this._ctx.textBeforeCursor;
   },
 
   get textAfterCursor() {
     return this._ctx.textAfterCursor;
   }
 };
 
- /**
+/**
+ * ==============================================
+ * HardwareInput
+ * ==============================================
+ */
+function MozHardwareInput() {
+}
+
+MozHardwareInput.prototype = {
+  classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
+  QueryInterface: XPCOMUtils.generateQI([]),
+};
+
+/**
  * ==============================================
  * InputContext
  * ==============================================
  */
 function MozInputContext(data) {
   this._context = {
     type: data.type,
     inputType: data.inputType,
@@ -802,29 +834,34 @@ function MozInputContext(data) {
   this._contextId = data.contextId;
 }
 
 MozInputContext.prototype = {
   _window: null,
   _context: null,
   _contextId: -1,
   _ipcHelper: null,
+  _hardwareinput: null,
+  _wrappedhardwareinput: null,
 
   classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ]),
 
   init: function ic_init(win) {
     this._window = win;
 
     this._ipcHelper = WindowMap.getInputContextIpcHelper(win);
     this._ipcHelper.attachInputContext(this);
+    this._hardwareinput = new MozHardwareInput();
+    this._wrappedhardwareinput =
+      this._window.MozHardwareInput._create(this._window, this._hardwareinput);
   },
 
   destroy: function ic_destroy() {
     // A consuming application might still hold a cached version of
     // this object. After destroying all methods will throw because we
     // cannot create new promises anymore, but we still hold
     // (outdated) information in the context. So let's clear that out.
     for (var k in this._context) {
@@ -832,16 +869,18 @@ MozInputContext.prototype = {
         this._context[k] = null;
       }
     }
 
     this._ipcHelper.detachInputContext();
     this._ipcHelper = null;
 
     this._window = null;
+    this._hardwareinput = null;
+    this._wrappedhardwareinput = null;
   },
 
   receiveMessage: function ic_receiveMessage(msg) {
     if (!msg || !msg.json) {
       dump('InputContext received message without data\n');
       return;
     }
 
@@ -984,16 +1023,20 @@ MozInputContext.prototype = {
 
   get textAfterCursor() {
     let text = this._context.text;
     let start = this._context.selectionStart;
     let end = this._context.selectionEnd;
     return text.substr(start, end - start + 100);
   },
 
+  get hardwareinput() {
+    return this._wrappedhardwareinput;
+  },
+
   setSelectionRange: function ic_setSelectionRange(start, length) {
     let self = this;
     return this._sendPromise(function(resolverId) {
       cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetSelectionRange', {
         contextId: self._contextId,
         requestId: resolverId,
         selectionStart: start,
         selectionEnd: start + length
@@ -1106,16 +1149,55 @@ MozInputContext.prototype = {
         contextId: self._contextId,
         requestId: resolverId,
         text: text || '',
         keyboardEventDict: this._getkeyboardEventDict(dict)
       });
     });
   },
 
+  // Generate a new keyboard event by the received keyboard dictionary
+  // and return defaultPrevented's result of the event after dispatching.
+  forwardHardwareKeyEvent: function ic_forwardHardwareKeyEvent(data) {
+    if (!Ci.nsIHardwareKeyHandler) {
+      return;
+    }
+
+    if (!this._context) {
+      return Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
+    }
+    let evt = new this._window.KeyboardEvent(data.type,
+                                             Cu.cloneInto(data.keyDict,
+                                                          this._window));
+    this._hardwareinput.__DOM_IMPL__.dispatchEvent(evt);
+    return this._getDefaultPreventedValue(evt);
+  },
+
+  _getDefaultPreventedValue: function(evt) {
+    if (!Ci.nsIHardwareKeyHandler) {
+      return;
+    }
+
+    let flags = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
+
+    if (evt.defaultPrevented) {
+      flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED;
+    }
+
+    if (evt.defaultPreventedByChrome) {
+      flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CHROME;
+    }
+
+    if (evt.defaultPreventedByContent) {
+      flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CONTENT;
+    }
+
+    return flags;
+  },
+
   _sendPromise: function(callback) {
     let self = this;
     return this._ipcHelper.createPromiseWithId(function(aResolverId) {
       if (!WindowMap.isActive(self._window)) {
         self._ipcHelper.removePromiseResolver(aResolverId);
         reject('Input method is not active.');
         return;
       }
--- a/dom/inputmethod/moz.build
+++ b/dom/inputmethod/moz.build
@@ -27,15 +27,15 @@ if CONFIG['MOZ_B2G']:
         '/layout/base',
     ]
 
 EXTRA_COMPONENTS += [
     'InputMethod.manifest',
     'MozKeyboard.js',
 ]
 
-EXTRA_JS_MODULES += [
+EXTRA_PP_JS_MODULES += [
     'Keyboard.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini']
--- a/dom/webidl/InputMethod.webidl
+++ b/dom/webidl/InputMethod.webidl
@@ -520,27 +520,45 @@ interface MozInputContext: EventTarget {
    * Note that composition always ends automatically with nothing to commit if
    * the composition does not explicitly end by calling |endComposition|, but
    * is interrupted by |sendKey|, |setSelectionRange|,
    * |replaceSurroundingText|, |deleteSurroundingText|, user moving the
    * cursor, changing the focus, etc.
    */
   Promise<boolean> endComposition(optional DOMString text,
                                   optional MozInputMethodKeyboardEventDict dict);
+
+  /**
+   * The interface used to receive the native events from hardware keyboard
+   */
+  readonly attribute MozHardwareInput? hardwareinput;
+};
+
+/*
+ * This interface will be added into inputcontext and used to receive the
+ * events from the hardware keyboard.
+ * Example:
+ *   mozInputMethod.inputcontext.hardwareinput.addEventListener('keyup', this);
+ *   mozInputMethod.inputcontext.hardwareinput.removeEventListener('keyup', this);
+ */
+[JSImplementation="@mozilla.org/b2g-hardwareinput;1",
+ Pref="dom.mozInputMethod.enabled",
+ CheckAnyPermissions="input"]
+interface MozHardwareInput: EventTarget {
 };
 
 /**
  * Detail of the selectionchange event.
  */
 [JSImplementation="@mozilla.org/b2g-imm-selectionchange;1",
  Pref="dom.mozInputMethod.enabled",
  CheckAnyPermissions="input"]
 interface MozInputContextSelectionChangeEventDetail {
   /**
-   * Indicate whether or not the change is due to our own action from, 
+   * Indicate whether or not the change is due to our own action from,
    * for example, sendKey() call.
    *
    * Note: this property is untrustworthy because it would still be true even
    * if script in the page changed the text synchronously upon responding to
    * events trigger by the call.
    */
   readonly attribute boolean ownAction;
 
@@ -554,17 +572,17 @@ interface MozInputContextSelectionChange
 /**
  * Detail of the surroundingtextchange event.
  */
 [JSImplementation="@mozilla.org/b2g-imm-surroundingtextchange;1",
  Pref="dom.mozInputMethod.enabled",
  CheckAnyPermissions="input"]
 interface MozInputContextSurroundingTextChangeEventDetail {
   /**
-   * Indicate whether or not the change is due to our own action from, 
+   * Indicate whether or not the change is due to our own action from,
    * for example, sendKey() call.
    *
    * Note: this property is untrustworthy because it would still be true even
    * if script in the page changed the text synchronously upon responding to
    * events trigger by the call.
    */
   readonly attribute boolean ownAction;