Bug 1197682 - InputMethodManager#setSupportsSwitchingTypes, r=janjongboom, sr=smaug
authorTim Chien <timdream@gmail.com>
Thu, 10 Sep 2015 22:29:00 +0200
changeset 296849 58a0ed06b69c78c15360ee5cb6abe2ce45802c73
parent 296848 3dec2b9352954b8acd9abb4f0d8cd6898da90d9d
child 296850 bc496e1c596316188b986f1c6c8ab632794d7e4e
push id962
push userjlund@mozilla.com
push dateFri, 04 Dec 2015 23:28:54 +0000
treeherdermozilla-release@23a2d286e80f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanjongboom, smaug
bugs1197682
milestone43.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 1197682 - InputMethodManager#setSupportsSwitchingTypes, r=janjongboom, sr=smaug
dom/inputmethod/Keyboard.jsm
dom/inputmethod/MozKeyboard.js
dom/inputmethod/mochitest/mochitest.ini
dom/inputmethod/mochitest/test_setSupportsSwitching.html
dom/webidl/InputMethod.webidl
--- a/dom/inputmethod/Keyboard.jsm
+++ b/dom/inputmethod/Keyboard.jsm
@@ -40,18 +40,20 @@ let Utils = {
   }
 };
 
 this.Keyboard = {
   _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.
-  _systemMessageName: [
-    'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions'
+  _supportsSwitchingTypes: [],
+  _systemMessageNames: [
+    'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
+    'SetSupportsSwitchingTypes'
   ],
 
   _messageNames: [
     'RemoveFocus',
     'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
     'SwitchToNextInputMethod', 'HideInputMethod',
     'GetText', 'SendKey', 'GetContext',
     'SetComposition', 'EndComposition',
@@ -65,16 +67,22 @@ this.Keyboard = {
     return null;
   },
 
   set formMM(mm) {
     this._formMM = mm;
   },
 
   sendToForm: function(name, data) {
+    if (!this.formMM) {
+      dump("Keyboard.jsm: Attempt to send message " + name +
+        " to form but no message manager exists.\n");
+
+      return;
+    }
     try {
       this.formMM.sendAsyncMessage(name, data);
     } catch(e) { }
   },
 
   sendToKeyboard: function(name, data) {
     try {
       this._keyboardMM.sendAsyncMessage(name, data);
@@ -86,17 +94,17 @@ this.Keyboard = {
     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 (let name of this._messageNames) {
       ppmm.addMessageListener('Keyboard:' + name, this);
     }
 
-    for (let name of this._systemMessageName) {
+    for (let name of this._systemMessageNames) {
       ppmm.addMessageListener('System:' + name, this);
     }
 
     this.inputRegistryGlue = new InputRegistryGlue();
   },
 
   observe: function keyboardObserve(subject, topic, data) {
     let frameLoader = null;
@@ -105,17 +113,17 @@ this.Keyboard = {
     if (topic == 'message-manager-close') {
       mm = subject;
     } else {
       frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
       mm = frameLoader.messageManager;
     }
 
     if (topic == 'oop-frameloader-crashed' ||
-	topic == 'message-manager-close') {
+	      topic == 'message-manager-close') {
       if (this.formMM == mm) {
         // The application has been closed unexpectingly. Let's tell the
         // keyboard app that the focus has been lost.
         this.sendToKeyboard('Keyboard:Blur', {});
         // Notify system app to hide keyboard.
         SystemAppProxy.dispatchEvent({
           type: 'inputmethod-contextchange',
           inputType: 'blur'
@@ -147,38 +155,36 @@ this.Keyboard = {
     mm.addMessageListener('Forms:SetComposition:Result:OK', this);
     mm.addMessageListener('Forms:EndComposition:Result:OK', this);
   },
 
   receiveMessage: function keyboardReceiveMessage(msg) {
     // If we get a 'Keyboard:XXX'/'System:XXX' message, check that the sender
     // has the required permission.
     let mm;
-    let isKeyboardRegistration = msg.name == "Keyboard:Register" ||
-                                 msg.name == "Keyboard:Unregister";
-    if (msg.name.indexOf("Keyboard:") === 0 ||
-        msg.name.indexOf("System:") === 0) {
-      if (!this.formMM && !isKeyboardRegistration) {
+
+    // Assert the permission based on the prefix of the message.
+    let permName;
+    if (msg.name.startsWith("Keyboard:")) {
+      permName = "input";
+    } else if (msg.name.startsWith("System:")) {
+      permName = "input-manage";
+    }
+
+    // There is no permission to check (nor we need to get the mm)
+    // for Form: messages.
+    if (permName) {
+      mm = Utils.getMMFromMessage(msg);
+      if (!mm) {
+        dump("Keyboard.jsm: Message " + msg.name + " has no message manager.");
         return;
       }
-
-      mm = Utils.getMMFromMessage(msg);
-
-      // That should never happen.
-      if (!mm) {
-        dump("!! No message manager found for " + msg.name);
-        return;
-      }
-
-      let perm = (msg.name.indexOf("Keyboard:") === 0) ? "input"
-                                                       : "input-manage";
-
-      if (!isKeyboardRegistration && !Utils.checkPermissionForMM(mm, perm)) {
-        dump("Keyboard message " + msg.name +
-        " from a content process with no '" + perm + "' privileges.");
+      if (!Utils.checkPermissionForMM(mm, permName)) {
+        dump("Keyboard.jsm: Message " + msg.name +
+          " from a content process with no '" + permName + "' privileges.");
         return;
       }
     }
 
     // we don't process kb messages (other than register)
     // if they come from a kb that we're currently not regsitered for.
     // this decision is made with the kbID kept by us and kb app
     let kbID = null;
@@ -224,16 +230,19 @@ this.Keyboard = {
         this.removeFocus();
         break;
       case 'System:SetSelectedOption':
         this.setSelectedOption(msg);
         break;
       case 'System:SetSelectedOptions':
         this.setSelectedOption(msg);
         break;
+      case 'System:SetSupportsSwitchingTypes':
+        this.setSupportsSwitchingTypes(msg);
+        break;
       case 'Keyboard:SetSelectionRange':
         this.setSelectionRange(msg);
         break;
       case 'Keyboard:ReplaceSurroundingText':
         this.replaceSurroundingText(msg);
         break;
       case 'Keyboard:SwitchToNextInputMethod':
         this.switchToNextInputMethod();
@@ -336,16 +345,20 @@ this.Keyboard = {
     this.sendToForm('Forms:SetSelectionRange', msg.data);
   },
 
   setValue: function keyboardSetValue(msg) {
     this.sendToForm('Forms:Input:Value', msg.data);
   },
 
   removeFocus: function keyboardRemoveFocus() {
+    if (!this.formMM) {
+      return;
+    }
+
     this.sendToForm('Forms:Select:Blur', {});
   },
 
   replaceSurroundingText: function keyboardReplaceSurroundingText(msg) {
     this.sendToForm('Forms:ReplaceSurroundingText', msg.data);
   },
 
   showInputMethodPicker: function keyboardShowInputMethodPicker() {
@@ -364,42 +377,57 @@ this.Keyboard = {
     this.sendToForm('Forms:GetText', msg.data);
   },
 
   sendKey: function keyboardSendKey(msg) {
     this.sendToForm('Forms:Input:SendKey', msg.data);
   },
 
   getContext: function keyboardGetContext(msg) {
-    if (this._layouts) {
-      this.sendToKeyboard('Keyboard:LayoutsChange', this._layouts);
+    if (!this.formMM) {
+      return;
     }
 
+    this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
+      types: this._supportsSwitchingTypes
+    });
+
     this.sendToForm('Forms:GetContext', msg.data);
   },
 
   setComposition: function keyboardSetComposition(msg) {
     this.sendToForm('Forms:SetComposition', msg.data);
   },
 
   endComposition: function keyboardEndComposition(msg) {
     this.sendToForm('Forms:EndComposition', msg.data);
   },
 
-  /**
-   * Get the number of keyboard layouts active from keyboard_manager
-   */
-  _layouts: null,
-  setLayouts: function keyboardSetLayoutCount(layouts) {
+  setSupportsSwitchingTypes: function setSupportsSwitchingTypes(msg) {
+    this._supportsSwitchingTypes = msg.data.types;
+    this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', msg.data);
+  },
+  // XXX: To be removed with mozContentEvent support from shell.js
+  setLayouts: function keyboardSetLayouts(layouts) {
     // The input method plugins may not have loaded yet,
     // cache the layouts so on init we can respond immediately instead
     // of going back and forth between keyboard_manager
-    this._layouts = layouts;
+    var types = [];
 
-    this.sendToKeyboard('Keyboard:LayoutsChange', layouts);
+    Object.keys(layouts).forEach((type) => {
+      if (layouts[type] > 1) {
+        types.push(type);
+      }
+    });
+
+    this._supportsSwitchingTypes = types;
+
+    this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
+      types: types
+    });
   }
 };
 
 function InputRegistryGlue() {
   this._messageId = 0;
   this._msgMap = new Map();
 
   ppmm.addMessageListener('InputRegistry:Add', this);
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -131,17 +131,17 @@ let cpmmSendAsyncMessageWithKbID = funct
  * InputMethodManager
  * ==============================================
  */
 function MozInputMethodManager(win) {
   this._window = win;
 }
 
 MozInputMethodManager.prototype = {
-  _supportsSwitching: false,
+  supportsSwitchingForCurrentInputContext: false,
   _window: null,
 
   classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
 
   QueryInterface: XPCOMUtils.generateQI([]),
 
   showAll: function() {
     if (!WindowMap.isActive(this._window)) {
@@ -156,40 +156,46 @@ MozInputMethodManager.prototype = {
     }
     cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SwitchToNextInputMethod', {});
   },
 
   supportsSwitching: function() {
     if (!WindowMap.isActive(this._window)) {
       return false;
     }
-    return this._supportsSwitching;
+    return this.supportsSwitchingForCurrentInputContext;
   },
 
   hide: function() {
     if (!WindowMap.isActive(this._window)) {
       return;
     }
     cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RemoveFocus', {});
+  },
+
+  setSupportsSwitchingTypes: function(types) {
+    cpmm.sendAsyncMessage('System:SetSupportsSwitchingTypes', {
+      types: types
+    });
   }
 };
 
 /**
  * ==============================================
  * InputMethod
  * ==============================================
  */
 function MozInputMethod() { }
 
 MozInputMethod.prototype = {
   __proto__: DOMRequestIpcHelper.prototype,
 
   _inputcontext: null,
   _wrappedInputContext: null,
-  _layouts: {},
+  _supportsSwitchingTypes: [],
   _window: null,
 
   classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIDOMGlobalPropertyInitializer,
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
@@ -203,30 +209,30 @@ MozInputMethod.prototype = {
                             .currentInnerWindowID;
 
     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:LayoutsChange', this);
+    cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
     cpmm.addWeakMessageListener('InputRegistry:Result:OK', this);
     cpmm.addWeakMessageListener('InputRegistry:Result:Error', this);
   },
 
   uninit: function mozInputMethodUninit() {
     this._window = null;
     this._mgmt = 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:LayoutsChange', this);
+    cpmm.removeWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
     cpmm.removeWeakMessageListener('InputRegistry:Result:OK', this);
     cpmm.removeWeakMessageListener('InputRegistry:Result:Error', this);
     this.setActive(false);
   },
 
   receiveMessage: function mozInputMethodReceiveMsg(msg) {
     if (!msg.name.startsWith('InputRegistry') &&
         !WindowMap.isActive(this._window)) {
@@ -248,18 +254,18 @@ MozInputMethod.prototype = {
       case 'Keyboard:SelectionChange':
         if (this.inputcontext) {
           this._inputcontext.updateSelectionContext(data, false);
         }
         break;
       case 'Keyboard:GetContext:Result:OK':
         this.setInputContext(data);
         break;
-      case 'Keyboard:LayoutsChange':
-        this._layouts = data;
+      case 'Keyboard:SupportsSwitchingTypesChange':
+        this._supportsSwitchingTypes = data.types;
         break;
 
       case 'InputRegistry:Result:OK':
         resolver.resolve();
 
         break;
 
       case 'InputRegistry:Result:Error':
@@ -294,23 +300,22 @@ MozInputMethod.prototype = {
     return this.__DOM_IMPL__.getEventHandler("oninputcontextchange");
   },
 
   setInputContext: function mozKeyboardContextChange(data) {
     if (this._inputcontext) {
       this._inputcontext.destroy();
       this._inputcontext = null;
       this._wrappedInputContext = null;
-      this._mgmt._supportsSwitching = false;
+      this._mgmt.supportsSwitchingForCurrentInputContext = false;
     }
 
     if (data) {
-      this._mgmt._supportsSwitching = this._layouts[data.inputType] ?
-        this._layouts[data.inputType] > 1 :
-        false;
+      this._mgmt.supportsSwitchingForCurrentInputContext =
+        (this._supportsSwitchingTypes.indexOf(data.inputType) !== -1);
 
       this._inputcontext = new MozInputContext(data);
       this._inputcontext.init(this._window);
       // inputcontext will be exposed as a WebIDL object. Create its
       // content-side object explicitly to avoid Bug 1001325.
       this._wrappedInputContext =
         this._window.MozInputContext._create(this._window, this._inputcontext);
     }
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -16,14 +16,15 @@ support-files =
 [test_bug953044.html]
 [test_bug960946.html]
 [test_bug978918.html]
 [test_bug1026997.html]
 [test_bug1043828.html]
 [test_bug1059163.html]
 [test_bug1066515.html]
 [test_bug1175399.html]
+[test_bug1137557.html]
 [test_sendkey_cancel.html]
+[test_setSupportsSwitching.html]
 [test_sync_edit.html]
 [test_two_inputs.html]
 [test_two_selects.html]
 [test_unload.html]
-[test_bug1137557.html]
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_setSupportsSwitching.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1197682
+-->
+<head>
+  <title>Test inputcontext#inputType and MozInputMethodManager#supportsSwitching()</title>
+  <script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197682">Mozilla Bug 1197682</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+  runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+  let input = content.document.body.firstElementChild;
+
+  let i = 1;
+
+  input.focus();
+
+  addMessageListener('test:next', function() {
+    i++;
+    switch (i) {
+      case 2:
+        content.document.body.children[1].focus();
+        i++; // keep the same count with the parent frame.
+
+        break;
+
+      case 4:
+        content.document.body.lastElementChild.focus();
+        i++; // keep the same count with the parent frame.
+
+        break;
+
+      case 6:
+        content.document.body.lastElementChild.blur();
+
+        break;
+    }
+  });
+};
+
+function runTest() {
+  let im = navigator.mozInputMethod;
+
+  let i = 0;
+  im.oninputcontextchange = function(evt) {
+    var inputcontext = navigator.mozInputMethod.inputcontext;
+
+    i++;
+    switch (i) {
+      case 1:
+        ok(!!inputcontext, '1) Receving the input context');
+        is(inputcontext.inputType, 'text', '1) input type');
+        is(im.mgmt.supportsSwitching(), true, '1) supports switching');
+
+        mm.sendAsyncMessage('test:next');
+        break;
+
+      case 2:
+        is(inputcontext, null, '2) Receving null inputcontext');
+
+        break;
+
+      case 3:
+        ok(!!inputcontext, '3) Receving the input context');
+        is(inputcontext.inputType, 'number', '3) input type');
+        is(im.mgmt.supportsSwitching(), false, '3) supports switching');
+
+        mm.sendAsyncMessage('test:next');
+        break;
+
+      case 4:
+        is(inputcontext, null, '4) Receving null inputcontext');
+
+        break;
+
+      case 5:
+        ok(!!inputcontext, '5) Receving the input context');
+        is(inputcontext.inputType, 'password', '5) input type');
+        is(im.mgmt.supportsSwitching(), true, '5) supports switching');
+
+        mm.sendAsyncMessage('test:next');
+        break;
+
+      case 6:
+        is(inputcontext, null, '6) Receving null inputcontext');
+        is(im.mgmt.supportsSwitching(), false, '6) supports switching');
+
+        inputmethod_cleanup();
+        break;
+
+      default:
+        ok(false, 'Receving extra inputcontextchange calls');
+        inputmethod_cleanup();
+
+        break;
+    }
+  };
+
+  // Set current page as an input method.
+  SpecialPowers.wrap(im).setActive(true);
+  // Set text and password inputs as supports switching (and not supported for number type)
+  im.mgmt.setSupportsSwitchingTypes(['text', 'password']);
+
+  let iframe = document.createElement('iframe');
+  iframe.src = 'data:text/html,<html><body>' +
+    '<input type="text">' +
+    '<input type="number">' +
+    '<input type="password">' +
+    '</body></html>';
+  iframe.setAttribute('mozbrowser', true);
+  document.body.appendChild(iframe);
+
+  let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+  iframe.addEventListener('mozbrowserloadend', function() {
+    mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+  });
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/webidl/InputMethod.webidl
+++ b/dom/webidl/InputMethod.webidl
@@ -114,48 +114,62 @@ interface MozInputMethod : EventTarget {
   void setSelectedOptions(sequence<long> indexes);
 };
 
 /**
  * InputMethodManager contains a few of the global methods for the input app.
  */
 [JSImplementation="@mozilla.org/b2g-imm;1",
  Pref="dom.mozInputMethod.enabled",
- CheckAnyPermissions="input"]
+ CheckAnyPermissions="input input-manage"]
 interface MozInputMethodManager {
   /**
    * Ask the OS to show a list of available inputs for users to switch from.
    * OS should sliently ignore this request if the app is currently not the
    * active one.
    */
+  [CheckAllPermissions="input"]
   void showAll();
 
   /**
    * Ask the OS to switch away from the current active input app.
    * OS should sliently ignore this request if the app is currently not the
    * active one.
    */
+  [CheckAllPermissions="input"]
   void next();
 
   /**
-   * If this method returns true, it is recommented that the input app provides
+   * If this method returns true, it is recommended that the input app provides
    * a shortcut that would invoke the next() method above, for easy switching
    * between inputs -- i.e. show a "global" button on screen if the input app
    * implements an on-screen virtual keyboard.
    *
    * The returning value is depend on the inputType of the current input context.
    */
+  [CheckAllPermissions="input"]
   boolean supportsSwitching();
 
   /**
    * Ask the OS to remove the input focus, will cause the lost of input context.
    * OS should sliently ignore this request if the app is currently not the
    * active one.
    */
+  [CheckAllPermissions="input"]
   void hide();
+
+  /**
+   * Update Gecko with information on the input types which supportsSwitching()
+   * should return ture.
+   *
+   * @param types Array of input types in which supportsSwitching() should
+   *              return true.
+   */
+  [CheckAllPermissions="input-manage"]
+  void setSupportsSwitchingTypes(sequence<MozInputMethodInputContextInputTypes> types);
 };
 
 /**
  * The input context, which consists of attributes and information of current
  * input field. It also hosts the methods available to the keyboard app to
  * mutate the input field represented. An "input context" gets void when the
  * app is no longer allowed to interact with the text field,
  * e.g., the text field does no longer exist, the app is being switched to