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 291026 2ba85d06b69b916f1e65c946a022b617b74ffb27
parent 291025 ebbd79395164168a51c850dc80c7daf156dfa99b
child 291027 f74a43d0b9eb6b7a3ae5e03c508eab14ac01620e
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki, smaug
bugs1110030
milestone48.0a1
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;