Bug 1201407 - Add input-manage-only events for InputMethod API. r=janjongboom, sr=smaug
authorTim Chien <timdream@gmail.com>
Wed, 16 Sep 2015 22:11:00 +0200
changeset 295676 310477a8720f081626b19e895ad01c2239bdb336
parent 295675 dca7021e514aa4bf7905439a3552f0265049c8f2
child 295677 94a7ca13ffa023d7fa5ef4728e6ef0d92ee2ad2e
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanjongboom, smaug
bugs1201407
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 1201407 - Add input-manage-only events for InputMethod API. r=janjongboom, sr=smaug
dom/inputmethod/Keyboard.jsm
dom/inputmethod/MozKeyboard.js
dom/inputmethod/forms.js
dom/inputmethod/mochitest/file_blank.html
dom/inputmethod/mochitest/file_inputmethod_1043828.html
dom/inputmethod/mochitest/mochitest.ini
dom/inputmethod/mochitest/test_bug1043828.html
dom/inputmethod/mochitest/test_focus_blur_manage_events.html
dom/inputmethod/mochitest/test_input_registry_events.html
dom/inputmethod/mochitest/test_simple_manage_events.html
dom/webidl/InputMethod.webidl
--- a/dom/inputmethod/Keyboard.jsm
+++ b/dom/inputmethod/Keyboard.jsm
@@ -40,29 +40,31 @@ var 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.
+  _systemMMs: [],     // The message managers registered to handle system async
+                      // messages.
   _supportsSwitchingTypes: [],
   _systemMessageNames: [
     'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
-    'SetSupportsSwitchingTypes'
+    'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister'
   ],
 
   _messageNames: [
     'RemoveFocus',
     'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
     'SwitchToNextInputMethod', 'HideInputMethod',
     'GetText', 'SendKey', 'GetContext',
     'SetComposition', 'EndComposition',
-    'Register', 'Unregister'
+    'RegisterSync', 'Unregister'
   ],
 
   get formMM() {
     if (this._formMM && !Cu.isDeadWrapper(this._formMM))
       return this._formMM;
 
     return null;
   },
@@ -84,16 +86,30 @@ this.Keyboard = {
   },
 
   sendToKeyboard: function(name, data) {
     try {
       this._keyboardMM.sendAsyncMessage(name, data);
     } catch(e) { }
   },
 
+  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;
+    }
+
+    this._systemMMs.forEach((mm, i) => {
+      data.inputManageId = i;
+      mm.sendAsyncMessage(name, data);
+    });
+  },
+
   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 (let name of this._messageNames) {
       ppmm.addMessageListener('Keyboard:' + name, this);
@@ -119,20 +135,24 @@ this.Keyboard = {
 
     if (topic == 'oop-frameloader-crashed' ||
 	      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.
+        this.sendToSystem('System:Blur', {});
+        // XXX: To be removed when content migrate away from mozChromeEvents.
         SystemAppProxy.dispatchEvent({
           type: 'inputmethod-contextchange',
           inputType: 'blur'
         });
+
+        this.formMM = null;
       }
     } else {
       // Ignore notifications that aren't from a BrowserOrApp
       if (!frameLoader.ownerIsBrowserOrAppFrame) {
         return;
       }
       this.initFormsFrameScript(mm);
     }
@@ -188,17 +208,17 @@ this.Keyboard = {
     // 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;
     if ('kbID' in msg.data) {
       kbID = msg.data.kbID;
     }
 
     if (0 === msg.name.indexOf('Keyboard:') &&
-        ('Keyboard:Register' !== msg.name && this._keyboardID !== kbID)
+        ('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID)
        ) {
       return;
     }
 
     switch (msg.name) {
       case 'Forms:Focus':
         this.handleFocus(msg);
         break;
@@ -224,16 +244,34 @@ this.Keyboard = {
 
       case 'System:SetValue':
         this.setValue(msg);
         break;
       case 'Keyboard:RemoveFocus':
       case 'System:RemoveFocus':
         this.removeFocus();
         break;
+      case 'System:RegisterSync': {
+        if (this._systemMMs.length !== 0) {
+          dump('Keyboard.jsm Warning: There are more than one content page ' +
+            'with input-manage permission. There will be undeterministic ' +
+            'responses to addInput()/removeInput() if both content pages are ' +
+            'trying to respond to the same request event.\n');
+        }
+
+        let id = this._systemMMs.length;
+        this._systemMMs.push(mm);
+
+        return id;
+      }
+
+      case 'System:Unregister':
+        this._systemMMs.splice(msg.data.id, 1);
+
+        break;
       case 'System:SetSelectedOption':
         this.setSelectedOption(msg);
         break;
       case 'System:SetSelectedOptions':
         this.setSelectedOption(msg);
         break;
       case 'System:SetSupportsSwitchingTypes':
         this.setSupportsSwitchingTypes(msg);
@@ -260,17 +298,17 @@ this.Keyboard = {
         this.getContext(msg);
         break;
       case 'Keyboard:SetComposition':
         this.setComposition(msg);
         break;
       case 'Keyboard:EndComposition':
         this.endComposition(msg);
         break;
-      case 'Keyboard:Register':
+      case 'Keyboard:RegisterSync':
         this._keyboardMM = mm;
         if (kbID) {
           // keyboard identifies itself, use its kbID
           // this msg would be async, so no need to return
           this._keyboardID = kbID;
         }else{
           // generate the id for the keyboard
           this._keyboardID = this._nextKeyboardID;
@@ -288,20 +326,24 @@ this.Keyboard = {
   },
 
   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 current active input app to gain focus.
     this.forwardEvent('Keyboard:Focus', msg);
 
-    // Chrome event, used also to render value selectors; that's why we need
-    // the info about choices / min / max here as well...
+    // 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.
     SystemAppProxy.dispatchEvent({
       type: 'inputmethod-contextchange',
       inputType: msg.data.inputType,
       value: msg.data.value,
       choices: JSON.stringify(msg.data.choices),
       min: msg.data.min,
       max: msg.data.max
     });
@@ -317,17 +359,19 @@ this.Keyboard = {
     if (mm !== this.formMM) {
       return;
     }
 
     // unset formMM
     this.formMM = null;
 
     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'
     });
   },
 
   forwardEvent: function keyboardForwardEvent(newEventName, msg) {
     this.sendToKeyboard(newEventName, msg.data);
@@ -357,22 +401,28 @@ this.Keyboard = {
     this.sendToForm('Forms:Select:Blur', {});
   },
 
   replaceSurroundingText: function keyboardReplaceSurroundingText(msg) {
     this.sendToForm('Forms:ReplaceSurroundingText', msg.data);
   },
 
   showInputMethodPicker: function keyboardShowInputMethodPicker() {
+    this.sendToSystem('System:ShowAll', {});
+
+    // XXX: To be removed with mozContentEvent support from shell.js
     SystemAppProxy.dispatchEvent({
       type: "inputmethod-showall"
     });
   },
 
   switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
+    this.sendToSystem('System:Next', {});
+
+    // XXX: To be removed with mozContentEvent support from shell.js
     SystemAppProxy.dispatchEvent({
       type: "inputmethod-next"
     });
   },
 
   getText: function keyboardGetText(msg) {
     this.sendToForm('Forms:GetText', msg.data);
   },
@@ -427,49 +477,66 @@ this.Keyboard = {
 };
 
 function InputRegistryGlue() {
   this._messageId = 0;
   this._msgMap = new Map();
 
   ppmm.addMessageListener('InputRegistry:Add', this);
   ppmm.addMessageListener('InputRegistry:Remove', this);
+  ppmm.addMessageListener('System:InputRegistry:Add:Done', this);
+  ppmm.addMessageListener('System:InputRegistry:Remove:Done', this);
 };
 
 InputRegistryGlue.prototype.receiveMessage = function(msg) {
   let mm = Utils.getMMFromMessage(msg);
 
-  if (!Utils.checkPermissionForMM(mm, 'input')) {
+  let permName = msg.name.startsWith("System:") ? "input-mgmt" : "input";
+  if (!Utils.checkPermissionForMM(mm, permName)) {
     dump("InputRegistryGlue message " + msg.name +
-      " from a content process with no 'input' privileges.");
+      " from a content process with no " + permName + " privileges.");
     return;
   }
 
   switch (msg.name) {
     case 'InputRegistry:Add':
       this.addInput(msg, mm);
 
       break;
 
     case 'InputRegistry:Remove':
       this.removeInput(msg, mm);
 
       break;
+
+    case 'System:InputRegistry:Add:Done':
+    case 'System:InputRegistry:Remove:Done':
+      this.returnMessage(msg.data);
+
+      break;
   }
 };
 
 InputRegistryGlue.prototype.addInput = function(msg, mm) {
   let msgId = this._messageId++;
   this._msgMap.set(msgId, {
     mm: mm,
     requestId: msg.data.requestId
   });
 
   let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
 
+  Keyboard.sendToSystem('System:InputRegistry:Add', {
+    id: msgId,
+    manifestURL: manifestURL,
+    inputId: msg.data.inputId,
+    inputManifest: msg.data.inputManifest
+  });
+
+  // XXX: To be removed when content migrate away from mozChromeEvents.
   SystemAppProxy.dispatchEvent({
     type: 'inputregistry-add',
     id: msgId,
     manifestURL: manifestURL,
     inputId: msg.data.inputId,
     inputManifest: msg.data.inputManifest
   });
 };
@@ -478,33 +545,43 @@ InputRegistryGlue.prototype.removeInput 
   let msgId = this._messageId++;
   this._msgMap.set(msgId, {
     mm: mm,
     requestId: msg.data.requestId
   });
 
   let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
 
+  Keyboard.sendToSystem('System:InputRegistry:Remove', {
+    id: msgId,
+    manifestURL: manifestURL,
+    inputId: msg.data.inputId
+  });
+
+  // XXX: To be removed when content migrate away from mozChromeEvents.
   SystemAppProxy.dispatchEvent({
     type: 'inputregistry-remove',
     id: msgId,
     manifestURL: manifestURL,
     inputId: msg.data.inputId
   });
 };
 
 InputRegistryGlue.prototype.returnMessage = function(detail) {
   if (!this._msgMap.has(detail.id)) {
+    dump('InputRegistryGlue: Ignoring already handled message response. ' +
+         'id=' + detail.id + '\n');
     return;
   }
 
   let { mm, requestId } = this._msgMap.get(detail.id);
   this._msgMap.delete(detail.id);
 
   if (Cu.isDeadWrapper(mm)) {
+    dump('InputRegistryGlue: Message manager has already died.\n');
     return;
   }
 
   if (!('error' in detail)) {
     mm.sendAsyncMessage('InputRegistry:Result:OK', {
       requestId: requestId
     });
   } else {
--- a/dom/inputmethod/MozKeyboard.js
+++ b/dom/inputmethod/MozKeyboard.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
+const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
   "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender");
 
@@ -138,16 +139,64 @@ function MozInputMethodManager(win) {
 MozInputMethodManager.prototype = {
   supportsSwitchingForCurrentInputContext: false,
   _window: null,
 
   classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
 
   QueryInterface: XPCOMUtils.generateQI([]),
 
+  set oninputcontextfocus(handler) {
+    this.__DOM_IMPL__.setEventHandler("oninputcontextfocus", handler);
+  },
+
+  get oninputcontextfocus() {
+    return this.__DOM_IMPL__.getEventHandler("oninputcontextfocus");
+  },
+
+  set oninputcontextblur(handler) {
+    this.__DOM_IMPL__.setEventHandler("oninputcontextblur", handler);
+  },
+
+  get oninputcontextblur() {
+    return this.__DOM_IMPL__.getEventHandler("oninputcontextblur");
+  },
+
+  set onshowallrequest(handler) {
+    this.__DOM_IMPL__.setEventHandler("onshowallrequest", handler);
+  },
+
+  get onshowallrequest() {
+    return this.__DOM_IMPL__.getEventHandler("onshowallrequest");
+  },
+
+  set onnextrequest(handler) {
+    this.__DOM_IMPL__.setEventHandler("onnextrequest", handler);
+  },
+
+  get onnextrequest() {
+    return this.__DOM_IMPL__.getEventHandler("onnextrequest");
+  },
+
+  set onaddinputrequest(handler) {
+    this.__DOM_IMPL__.setEventHandler("onaddinputrequest", handler);
+  },
+
+  get onaddinputrequest() {
+    return this.__DOM_IMPL__.getEventHandler("onaddinputrequest");
+  },
+
+  set onremoveinputrequest(handler) {
+    this.__DOM_IMPL__.setEventHandler("onremoveinputrequest", handler);
+  },
+
+  get onremoveinputrequest() {
+    return this.__DOM_IMPL__.getEventHandler("onremoveinputrequest");
+  },
+
   showAll: function() {
     if (!WindowMap.isActive(this._window)) {
       return;
     }
     cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ShowInputMethodPicker', {});
   },
 
   next: function() {
@@ -170,81 +219,278 @@ MozInputMethodManager.prototype = {
     }
     cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RemoveFocus', {});
   },
 
   setSupportsSwitchingTypes: function(types) {
     cpmm.sendAsyncMessage('System:SetSupportsSwitchingTypes', {
       types: types
     });
+  },
+
+  handleFocus: function(data) {
+    let detail = new MozInputContextFocusEventDetail(this._window, data);
+    let wrappedDetail =
+      this._window.MozInputContextFocusEventDetail._create(this._window, detail);
+    let event = new this._window.CustomEvent('inputcontextfocus',
+      { cancelable: true, detail: wrappedDetail });
+
+    let handled = !this.__DOM_IMPL__.dispatchEvent(event);
+
+    // A gentle warning if the event is not preventDefault() by the content.
+    if (!handled) {
+      dump('MozKeyboard.js: A frame with input-manage permission did not' +
+        ' handle the inputcontextfocus event dispatched.\n');
+    }
+  },
+
+  handleBlur: function(data) {
+    let event =
+      new this._window.Event('inputcontextblur', { cancelable: true });
+
+    let handled = !this.__DOM_IMPL__.dispatchEvent(event);
+
+    // A gentle warning if the event is not preventDefault() by the content.
+    if (!handled) {
+      dump('MozKeyboard.js: A frame with input-manage permission did not' +
+        ' handle the inputcontextblur event dispatched.\n');
+    }
+  },
+
+  dispatchShowAllRequestEvent: function() {
+    this._fireSimpleEvent('showallrequest');
+  },
+
+  dispatchNextRequestEvent: function() {
+    this._fireSimpleEvent('nextrequest');
+  },
+
+  _fireSimpleEvent: function(eventType) {
+    let event = new this._window.Event(eventType);
+    let handled = !this.__DOM_IMPL__.dispatchEvent(event, { cancelable: true });
+
+    // A gentle warning if the event is not preventDefault() by the content.
+    if (!handled) {
+      dump('MozKeyboard.js: A frame with input-manage permission did not' +
+        ' handle the ' + eventType + ' event dispatched.\n');
+    }
+  },
+
+  handleAddInput: function(data) {
+    let p = this._fireInputRegistryEvent('addinputrequest', data);
+    if (!p) {
+      return;
+    }
+
+    p.then(() => {
+      cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', {
+        id: data.id
+      });
+    }, (error) => {
+      cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', {
+        id: data.id,
+        error: error || 'Unknown Error'
+      });
+    });
+  },
+
+  handleRemoveInput: function(data) {
+    let p = this._fireInputRegistryEvent('removeinputrequest', data);
+    if (!p) {
+      return;
+    }
+
+    p.then(() => {
+      cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', {
+        id: data.id
+      });
+    }, (error) => {
+      cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', {
+        id: data.id,
+        error: error || 'Unknown Error'
+      });
+    });
+  },
+
+  _fireInputRegistryEvent: function(eventType, data) {
+    let detail = new MozInputRegistryEventDetail(this._window, data);
+    let wrappedDetail =
+      this._window.MozInputRegistryEventDetail._create(this._window, detail);
+    let event = new this._window.CustomEvent(eventType,
+      { cancelable: true, detail: wrappedDetail });
+    let handled = !this.__DOM_IMPL__.dispatchEvent(event);
+
+    // A gentle warning if the event is not preventDefault() by the content.
+    if (!handled) {
+      dump('MozKeyboard.js: A frame with input-manage permission did not' +
+        ' handle the ' + eventType + ' event dispatched.\n');
+
+      return null;
+    }
+    return detail.takeChainedPromise();
+  }
+};
+
+function MozInputContextFocusEventDetail(win, data) {
+  this.type = data.type;
+  this.inputType = data.inputType;
+  this.value = data.value;
+  // Exposed as MozInputContextChoicesInfo dictionary defined in WebIDL
+  this.choices = data.choices;
+  this.min = data.min;
+  this.max = data.max;
+}
+MozInputContextFocusEventDetail.prototype = {
+  classID: Components.ID("{e0794208-ac50-40e8-b22e-6ee0b4c4e6e8}"),
+  QueryInterface: XPCOMUtils.generateQI([]),
+
+  type: undefined,
+  inputType: undefined,
+  value: '',
+  choices: null,
+  min: undefined,
+  max: undefined
+};
+
+function MozInputRegistryEventDetail(win, data) {
+  this._window = win;
+
+  this.manifestURL = data.manifestURL;
+  this.inputId = data.inputId;
+  // Exposed as MozInputMethodInputManifest dictionary defined in WebIDL
+  this.inputManifest = data.inputManifest;
+
+  this._chainedPromise = Promise.resolve();
+}
+MozInputRegistryEventDetail.prototype = {
+  classID: Components.ID("{02130070-9b3e-4f38-bbd9-f0013aa36717}"),
+  QueryInterface: XPCOMUtils.generateQI([]),
+
+  _window: null,
+
+  manifestURL: undefined,
+  inputId: undefined,
+  inputManifest: null,
+
+  waitUntil: function(p) {
+    // Need an extra protection here since waitUntil will be an no-op
+    // when chainedPromise is already returned.
+    if (!this._chainedPromise) {
+      throw new this._window.DOMException(
+        'Must call waitUntil() within the event handling loop.',
+        'InvalidStateError');
+    }
+
+    this._chainedPromise = this._chainedPromise
+      .then(function() { return p; });
+  },
+
+  takeChainedPromise: function() {
+    var p = this._chainedPromise;
+    this._chainedPromise = null;
+    return p;
   }
 };
 
 /**
  * ==============================================
  * InputMethod
  * ==============================================
  */
 function MozInputMethod() { }
 
 MozInputMethod.prototype = {
   __proto__: DOMRequestIpcHelper.prototype,
 
+  _window: null,
   _inputcontext: null,
   _wrappedInputContext: null,
+  _mgmt: null,
+  _wrappedMgmt: null,
   _supportsSwitchingTypes: [],
-  _window: null,
+  _inputManageId: undefined,
 
   classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIDOMGlobalPropertyInitializer,
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ]),
 
   init: function mozInputMethodInit(win) {
     this._window = win;
     this._mgmt = new MozInputMethodManager(win);
+    this._wrappedMgmt = win.MozInputMethodManager._create(win, this._mgmt);
     this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIDOMWindowUtils)
                             .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:SupportsSwitchingTypesChange', 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);
+      cpmm.addWeakMessageListener('System:Next', this);
+      cpmm.addWeakMessageListener('System:InputRegistry:Add', this);
+      cpmm.addWeakMessageListener('System:InputRegistry:Remove', this);
+    }
   },
 
   uninit: function mozInputMethodUninit() {
     this._window = null;
     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('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
+      });
+      cpmm.removeWeakMessageListener('System:Focus', this);
+      cpmm.removeWeakMessageListener('System:Blur', this);
+      cpmm.removeWeakMessageListener('System:ShowAll', this);
+      cpmm.removeWeakMessageListener('System:Next', this);
+      cpmm.removeWeakMessageListener('System:InputRegistry:Add', this);
+      cpmm.removeWeakMessageListener('System:InputRegistry:Remove', this);
+    }
   },
 
   receiveMessage: function mozInputMethodReceiveMsg(msg) {
-    if (!msg.name.startsWith('InputRegistry') &&
+    if (msg.name.startsWith('Keyboard') &&
         !WindowMap.isActive(this._window)) {
       return;
     }
 
     let data = msg.data;
+
+    if (msg.name.startsWith('System') &&
+      this._inputManageId !== data.inputManageId) {
+      return;
+    }
+    delete data.inputManageId;
+
     let resolver = ('requestId' in data) ?
       this.takePromiseResolver(data.requestId) : null;
 
     switch(msg.name) {
       case 'Keyboard:Focus':
         // XXX Bug 904339 could receive 'text' event twice
         this.setInputContext(data);
         break;
@@ -267,27 +513,51 @@ MozInputMethod.prototype = {
         resolver.resolve();
 
         break;
 
       case 'InputRegistry:Result:Error':
         resolver.reject(data.error);
 
         break;
+
+      case 'System:Focus':
+        this._mgmt.handleFocus(data);
+        break;
+
+      case 'System:Blur':
+        this._mgmt.handleBlur(data);
+        break;
+
+      case 'System:ShowAll':
+        this._mgmt.dispatchShowAllRequestEvent();
+        break;
+
+      case 'System:Next':
+        this._mgmt.dispatchNextRequestEvent();
+        break;
+
+      case 'System:InputRegistry:Add':
+        this._mgmt.handleAddInput(data);
+        break;
+
+      case 'System:InputRegistry:Remove':
+        this._mgmt.handleRemoveInput(data);
+        break;
     }
   },
 
   observe: function mozInputMethodObserve(subject, topic, data) {
     let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
     if (wId == this.innerWindowID)
       this.uninit();
   },
 
   get mgmt() {
-    return this._mgmt;
+    return this._wrappedMgmt;
   },
 
   get inputcontext() {
     if (!WindowMap.isActive(this._window)) {
       return null;
     }
     return this._wrappedInputContext;
   },
@@ -315,18 +585,17 @@ MozInputMethod.prototype = {
       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);
     }
 
-    let event = new this._window.Event("inputcontextchange",
-                                       Cu.cloneInto({}, this._window));
+    let event = new this._window.Event("inputcontextchange");
     this.__DOM_IMPL__.dispatchEvent(event);
   },
 
   setActive: function mozInputMethodSetActive(isActive) {
     if (WindowMap.isActive(this._window) === isActive) {
       return;
     }
 
@@ -339,19 +608,19 @@ MozInputMethod.prototype = {
       // Otherwise silently ignored.
 
       // get keyboard ID from Keyboard.jsm,
       // or if we already have it, get it from our map
       // Note: if we need to get it from Keyboard.jsm,
       // we have to use a synchronous message
       var kbID = WindowMap.getKbID(this._window);
       if (kbID) {
-        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Register', {});
+        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RegisterSync', {});
       } else {
-        let res = cpmm.sendSyncMessage('Keyboard:Register', {});
+        let res = cpmm.sendSyncMessage('Keyboard:RegisterSync', {});
         WindowMap.setKbID(this._window, res[0]);
       }
 
       cpmmSendAsyncMessageWithKbID(this, 'Keyboard:GetContext', {});
     } else {
       // Deactive current input method.
       cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Unregister', {});
       if (this._inputcontext) {
@@ -400,16 +669,23 @@ MozInputMethod.prototype = {
   setSelectedOptions: function(indexes) {
     cpmm.sendAsyncMessage('System:SetSelectedOptions', {
       'indexes': indexes
     });
   },
 
   removeFocus: function() {
     cpmm.sendAsyncMessage('System:RemoveFocus', {});
+  },
+
+  _hasInputManagePerm: function(win) {
+    let principal = win.document.nodePrincipal;
+    let perm = Services.perms.testExactPermissionFromPrincipal(principal,
+                                                               "input-manage");
+    return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
   }
 };
 
  /**
  * ==============================================
  * InputContextDOMRequestIpcHelper
  * ==============================================
  */
--- a/dom/inputmethod/forms.js
+++ b/dom/inputmethod/forms.js
@@ -1130,16 +1130,18 @@ function getJSON(element, focusCounter) 
   // let's return their real type even if the platform returns 'text'
   let attributeInputType = element.getAttribute("type") || "";
 
   if (attributeInputType) {
     let inputTypeLowerCase = attributeInputType.toLowerCase();
     switch (inputTypeLowerCase) {
       case "datetime":
       case "datetime-local":
+      case "month":
+      case "week":
       case "range":
         inputType = inputTypeLowerCase;
         break;
     }
   }
 
   // Gecko has some support for @inputmode but behind a preference and
   // it is disabled by default.
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_blank.html
@@ -0,0 +1,4 @@
+<html>
+<body>
+</body>
+</html>
deleted file mode 100644
--- a/dom/inputmethod/mochitest/file_inputmethod_1043828.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<html>
-<body>
-</body>
-</html>
--- a/dom/inputmethod/mochitest/mochitest.ini
+++ b/dom/inputmethod/mochitest/mochitest.ini
@@ -1,15 +1,15 @@
 [DEFAULT]
 # Not supported on Android, bug 983015 for B2G emulator
 skip-if = (toolkit == 'android' || toolkit == 'gonk') || e10s
 support-files =
   inputmethod_common.js
   file_inputmethod.html
-  file_inputmethod_1043828.html
+  file_blank.html
   file_test_app.html
   file_test_sendkey_cancel.html
   file_test_sms_app.html
   file_test_sms_app_1066515.html
 
 [test_basic.html]
 [test_bug944397.html]
 [test_bug949059.html]
@@ -17,14 +17,17 @@ support-files =
 [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_focus_blur_manage_events.html]
+[test_input_registry_events.html]
 [test_sendkey_cancel.html]
 [test_setSupportsSwitching.html]
+[test_simple_manage_events.html]
 [test_sync_edit.html]
 [test_two_inputs.html]
 [test_two_selects.html]
 [test_unload.html]
--- a/dom/inputmethod/mochitest/test_bug1043828.html
+++ b/dom/inputmethod/mochitest/test_bug1043828.html
@@ -79,17 +79,17 @@ function runTest() {
     keyboardA.setAttribute('mozbrowser', true);
     document.body.appendChild(keyboardA);
 
     keyboardB = document.createElement('iframe');
     keyboardB.setAttribute('mozbrowser', true);
     document.body.appendChild(keyboardB);
 
     // simulate two different keyboard apps
-    let imeUrl = basePath + '/file_inputmethod_1043828.html';
+    let imeUrl = basePath + '/file_blank.html';
 
     SpecialPowers.pushPermissions([{
       type: 'input',
       allow: true,
       context: {
         url: imeUrl,
         appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
         isInBrowserElement: true
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_focus_blur_manage_events.html
@@ -0,0 +1,230 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
+-->
+<head>
+  <title>Test inputcontextfocus and inputcontextblur event</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=1201407">Mozilla Bug 1201407</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+let contentFrameMM;
+
+function setupTestRunner() {
+  info('setupTestRunner');
+  let im = navigator.mozInputMethod;
+
+  let expectedEventDetails = [
+    { type: 'input', inputType: 'text' },
+    { type: 'input', inputType: 'search' },
+    { type: 'textarea', inputType: 'textarea' },
+    { type: 'contenteditable', inputType: 'textarea' },
+    { type: 'input', inputType: 'number' },
+    { type: 'input', inputType: 'tel' },
+    { type: 'input', inputType: 'url' },
+    { type: 'input', inputType: 'email' },
+    { type: 'input', inputType: 'password' },
+    { type: 'input', inputType: 'datetime' },
+    { type: 'input', inputType: 'date',
+      value: '2015-08-03', min: '1990-01-01', max: '2020-01-01' },
+    { type: 'input', inputType: 'month' },
+    { type: 'input', inputType: 'week' },
+    { type: 'input', inputType: 'time' },
+    { type: 'input', inputType: 'datetime-local' },
+    { type: 'input', inputType: 'color' },
+    { type: 'select', inputType: 'select-one',
+      choices: {
+        multiple: false,
+        choices: [
+          { group: false, inGroup: false, text: 'foo',
+            disabled: false, selected: true, optionIndex: 0 },
+          { group: false, inGroup: false, text: 'bar',
+            disabled: true, selected: false, optionIndex: 1 },
+          { group: true, text: 'group', disabled: false },
+          { group: false, inGroup: true, text: 'baz',
+            disabled: false, selected: false, optionIndex: 2 } ] }
+    },
+    { type: 'select', inputType: 'select-multiple',
+      choices: {
+        multiple: true,
+        choices: [
+          { group: false, inGroup: false, text: 'foo',
+            disabled: false, selected: true, optionIndex: 0 },
+          { group: false, inGroup: false, text: 'bar',
+            disabled: true, selected: false, optionIndex: 1 },
+          { group: true, text: 'group', disabled: false },
+          { group: false, inGroup: true, text: 'baz',
+            disabled: false, selected: false, optionIndex: 2 } ] }
+    }
+  ];
+
+  let expectBlur = false;
+
+  function deepAssertObject(obj, expectedObj, desc) {
+    for (let prop in expectedObj) {
+      if (typeof expectedObj[prop] === 'object') {
+        deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop);
+      } else {
+        is(obj[prop], expectedObj[prop], desc + '.' + prop);
+      }
+    }
+  }
+
+  im.mgmt.oninputcontextfocus =
+  im.mgmt.oninputcontextblur = function(evt) {
+    if (expectBlur) {
+      is(evt.type, 'inputcontextblur', 'evt.type');
+      evt.preventDefault();
+      expectBlur = false;
+
+      return;
+    }
+
+    let expectedEventDetail = expectedEventDetails.shift();
+
+    if (!expectedEventDetail) {
+        ok(false, 'Receving extra events');
+        inputmethod_cleanup();
+
+      return;
+    }
+
+    is(evt.type, 'inputcontextfocus', 'evt.type');
+    evt.preventDefault();
+    expectBlur = true;
+
+    let detail = evt.detail;
+    deepAssertObject(detail, expectedEventDetail, 'detail');
+
+    if (expectedEventDetails.length) {
+      contentFrameMM.sendAsyncMessage('test:next');
+    } else {
+      im.mgmt.oninputcontextfocus = im.mgmt.oninputcontextblur = null;
+      inputmethod_cleanup();
+    }
+  };
+}
+
+function setupInputAppFrame() {
+  info('setupInputAppFrame');
+  return new Promise((resolve, reject) => {
+    let appFrameScript = function appFrameScript() {
+      let im = content.navigator.mozInputMethod;
+
+      im.mgmt.oninputcontextfocus =
+      im.mgmt.oninputcontextblur = function(evt) {
+        sendAsyncMessage('text:appEvent', { type: evt.type });
+      };
+
+      content.document.body.textContent = 'I am a input app';
+    };
+
+    let path = location.pathname;
+    let basePath = location.protocol + '//' + location.host +
+                 path.substring(0, path.lastIndexOf('/'));
+    let imeUrl = basePath + '/file_blank.html';
+
+    let inputAppFrame = document.createElement('iframe');
+    inputAppFrame.setAttribute('mozbrowser', true);
+    inputAppFrame.src = imeUrl;
+    document.body.appendChild(inputAppFrame);
+
+    SpecialPowers.pushPermissions([{
+      type: 'input',
+      allow: true,
+      context: {
+        url: imeUrl,
+        appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
+        isInBrowserElement: true
+      }
+    }], function() {
+      let mm = SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
+      inputAppFrame.addEventListener('mozbrowserloadend', function() {
+        mm.addMessageListener('text:appEvent', function(msg) {
+          ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
+        });
+        mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+        // Set the input app frame to be active
+        let req = inputAppFrame.setInputMethodActive(true);
+        resolve(req);
+      });
+    });
+  });
+}
+
+function setupContentFrame() {
+  info('setupContentFrame');
+  return new Promise((resolve, reject) => {
+    let contentFrameScript = function contentFrameScript() {
+      let input = content.document.body.firstElementChild;
+
+      let i = 0;
+
+      input.focus();
+
+      addMessageListener('test:next', function() {
+        content.document.body.children[++i].focus();
+      });
+    };
+
+    let iframe = document.createElement('iframe');
+    iframe.src = 'data:text/html,<html><body>' +
+      '<input type="text">' +
+      '<input type="search">' +
+      '<textarea></textarea>' +
+      '<p contenteditable></p>' +
+      '<input type="number">' +
+      '<input type="tel">' +
+      '<input type="url">' +
+      '<input type="email">' +
+      '<input type="password">' +
+      '<input type="datetime">' +
+      '<input type="date" value="2015-08-03" min="1990-01-01" max="2020-01-01">' +
+      '<input type="month">' +
+      '<input type="week">' +
+      '<input type="time">' +
+      '<input type="datetime-local">' +
+      '<input type="color">' +
+      '<select><option selected>foo</option><option disabled>bar</option>' +
+        '<optgroup label="group"><option>baz</option></optgroup></select>' +
+      '<select multiple><option selected>foo</option><option disabled>bar</option>' +
+        '<optgroup label="group"><option>baz</option></optgroup></select>' +
+      '</body></html>';
+    iframe.setAttribute('mozbrowser', true);
+    document.body.appendChild(iframe);
+
+    let mm = contentFrameMM =
+      SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+    iframe.addEventListener('mozbrowserloadend', function() {
+      mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false);
+
+      resolve();
+    });
+  });
+}
+
+inputmethod_setup(function() {
+  Promise.resolve()
+    .then(() => setupTestRunner())
+    .then(() => setupContentFrame())
+    .then(() => setupInputAppFrame())
+    .catch((e) => {
+      ok(false, 'Error' + e.toString());
+      console.error(e);
+    });
+});
+
+</script>
+</pre>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_input_registry_events.html
@@ -0,0 +1,259 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
+-->
+<head>
+  <title>Test addinputrequest and removeinputrequest event</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=1201407">Mozilla Bug 1201407</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+let appFrameMM;
+let nextStep;
+
+function setupInputAppFrame() {
+  info('setupInputAppFrame');
+  return new Promise((resolve, reject) => {
+    let appFrameScript = function appFrameScript() {
+      let im = content.navigator.mozInputMethod;
+
+      addMessageListener('test:callAddInput', function() {
+        im.addInput('foo', {
+            launch_path: 'bar.html',
+            name: 'Foo',
+            description: 'foobar',
+            types: ['text', 'password']
+          })
+          .then((r) => {
+              sendAsyncMessage('test:resolved', { resolved: true, result: r });
+            }, (e) => {
+              sendAsyncMessage('test:rejected', { rejected: true, error: e });
+            });
+      });
+
+      addMessageListener('test:callRemoveInput', function() {
+        im.removeInput('foo')
+          .then((r) => {
+              sendAsyncMessage('test:resolved', { resolved: true, result: r });
+            }, (e) => {
+              sendAsyncMessage('test:rejected', { rejected: true, error: e });
+            });
+      });
+
+      im.mgmt.onaddinputrequest =
+      im.mgmt.onremoveinputrequest = function(evt) {
+        sendAsyncMessage('test:appEvent', { type: evt.type });
+      };
+
+      content.document.body.textContent = 'I am a input app';
+    };
+
+    let path = location.pathname;
+    let basePath = location.protocol + '//' + location.host +
+                 path.substring(0, path.lastIndexOf('/'));
+    let imeUrl = basePath + '/file_blank.html';
+
+    let inputAppFrame = document.createElement('iframe');
+    inputAppFrame.setAttribute('mozbrowser', true);
+    inputAppFrame.src = imeUrl;
+    document.body.appendChild(inputAppFrame);
+
+    SpecialPowers.pushPermissions([{
+      type: 'input',
+      allow: true,
+      context: {
+        url: imeUrl,
+        appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
+        isInBrowserElement: true
+      }
+    }], function() {
+      let mm = appFrameMM =
+        SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
+
+      inputAppFrame.addEventListener('mozbrowserloadend', function() {
+        mm.addMessageListener('test:appEvent', function(msg) {
+          ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
+        });
+        mm.addMessageListener('test:resolved', function(msg) {
+          nextStep && nextStep(msg.data);
+        });
+        mm.addMessageListener('test:rejected', function(msg) {
+          nextStep && nextStep(msg.data);
+        });
+        mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+        resolve();
+      });
+    });
+  });
+}
+
+function Deferred() {
+  this.promise = new Promise((res, rej) => {
+    this.resolve = res;
+    this.reject = rej;
+  });
+  return this;
+}
+
+function deepAssertObject(obj, expectedObj, desc) {
+  for (let prop in expectedObj) {
+    if (typeof expectedObj[prop] === 'object') {
+      deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop);
+    } else {
+      is(obj[prop], expectedObj[prop], desc + '.' + prop);
+    }
+  }
+}
+
+function setupTestRunner() {
+  let im = navigator.mozInputMethod;
+  let d;
+
+  let i = -1;
+  nextStep = function next(evt) {
+    i++;
+    info('Step ' + i);
+
+    switch (i) {
+      case 0:
+        appFrameMM.sendAsyncMessage('test:callAddInput');
+
+        break;
+
+      case 1:
+        is(evt.type, 'addinputrequest', 'evt.type');
+        deepAssertObject(evt.detail, {
+          inputId: 'foo',
+          manifestURL: null, // todo
+          inputManifest: {
+            launch_path: 'bar.html',
+            name: 'Foo',
+            description: 'foobar',
+            types: ['text', 'password']
+          }
+        }, 'detail');
+
+        d = new Deferred();
+        evt.detail.waitUntil(d.promise);
+        evt.preventDefault();
+
+        Promise.resolve().then(next);
+        break;
+
+      case 2:
+        d.resolve();
+        d = null;
+        break;
+
+      case 3:
+        ok(evt.resolved, 'resolved');
+        appFrameMM.sendAsyncMessage('test:callAddInput');
+
+        break;
+
+      case 4:
+        is(evt.type, 'addinputrequest', 'evt.type');
+
+        d = new Deferred();
+        evt.detail.waitUntil(d.promise);
+        evt.preventDefault();
+
+        Promise.resolve().then(next);
+        break;
+
+      case 5:
+        d.reject('Foo Error');
+        d = null;
+        break;
+
+      case 6:
+        ok(evt.rejected, 'rejected');
+        is(evt.error, 'Foo Error', 'rejected');
+
+
+        appFrameMM.sendAsyncMessage('test:callRemoveInput');
+
+        break;
+
+      case 7:
+        is(evt.type, 'removeinputrequest', 'evt.type');
+        deepAssertObject(evt.detail, {
+          inputId: 'foo',
+          manifestURL: null // todo
+        }, 'detail');
+
+        d = new Deferred();
+        evt.detail.waitUntil(d.promise);
+        evt.preventDefault();
+
+        Promise.resolve().then(next);
+        break;
+
+      case 8:
+        d.resolve();
+        d = null;
+        break;
+
+      case 9:
+        ok(evt.resolved, 'resolved');
+        appFrameMM.sendAsyncMessage('test:callRemoveInput');
+
+        break;
+
+      case 10:
+        is(evt.type, 'removeinputrequest', 'evt.type');
+
+        d = new Deferred();
+        evt.detail.waitUntil(d.promise);
+        evt.preventDefault();
+
+        Promise.resolve().then(next);
+        break;
+
+      case 11:
+        d.reject('Foo Error');
+        d = null;
+        break;
+
+      case 12:
+        ok(evt.rejected, 'rejected');
+        is(evt.error, 'Foo Error', 'rejected');
+        inputmethod_cleanup();
+
+        break;
+
+      default:
+        ok(false, 'received extra call.');
+        inputmethod_cleanup();
+
+        break;
+    }
+  }
+
+  im.mgmt.onaddinputrequest =
+  im.mgmt.onremoveinputrequest = nextStep;
+}
+
+inputmethod_setup(function() {
+  Promise.resolve()
+    .then(() => setupTestRunner())
+    .then(() => setupInputAppFrame())
+    .then(() => nextStep())
+    .catch((e) => {
+      ok(false, 'Error' + e.toString());
+      console.error(e);
+    });
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_simple_manage_events.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
+-->
+<head>
+  <title>Test simple manage notification events on MozInputMethodManager</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=1201407">Mozilla Bug 1201407</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+let appFrameMM;
+let nextStep;
+
+function setupTestRunner() {
+  info('setupTestRunner');
+  let im = navigator.mozInputMethod;
+
+  let i = 0;
+  im.mgmt.onshowallrequest =
+  im.mgmt.onnextrequest = nextStep = function(evt) {
+    i++;
+    switch (i) {
+      case 1:
+        is(evt.type, 'inputcontextchange', '1) inputcontextchange event');
+        appFrameMM.sendAsyncMessage('test:callShowAll');
+
+        break;
+
+      case 2:
+        is(evt.type, 'showallrequest', '2) showallrequest event');
+        ok(evt.target, im.mgmt, '2) evt.target');
+        evt.preventDefault();
+
+        appFrameMM.sendAsyncMessage('test:callNext');
+
+        break;
+
+      case 3:
+        is(evt.type, 'nextrequest', '3) nextrequest event');
+        ok(evt.target, im.mgmt, '3) evt.target');
+        evt.preventDefault();
+
+        im.mgmt.onshowallrequest =
+        im.mgmt.onnextrequest = nextStep = null;
+
+        inputmethod_cleanup();
+        break;
+
+      default:
+        ok(false, 'Receving extra events');
+        inputmethod_cleanup();
+
+        break;
+    }
+  };
+}
+
+function setupInputAppFrame() {
+  info('setupInputAppFrame');
+  return new Promise((resolve, reject) => {
+    let appFrameScript = function appFrameScript() {
+      let im = content.navigator.mozInputMethod;
+
+      addMessageListener('test:callShowAll', function() {
+        im.mgmt.showAll();
+      });
+
+      addMessageListener('test:callNext', function() {
+        im.mgmt.next();
+      });
+
+      im.mgmt.onshowallrequest =
+      im.mgmt.onnextrequest = function(evt) {
+        sendAsyncMessage('test:appEvent', { type: evt.type });
+      };
+
+      im.oninputcontextchange = function(evt) {
+        sendAsyncMessage('test:inputcontextchange', {});
+      };
+
+      content.document.body.textContent = 'I am a input app';
+    };
+
+    let path = location.pathname;
+    let basePath = location.protocol + '//' + location.host +
+                 path.substring(0, path.lastIndexOf('/'));
+    let imeUrl = basePath + '/file_blank.html';
+
+    let inputAppFrame = document.createElement('iframe');
+    inputAppFrame.setAttribute('mozbrowser', true);
+    inputAppFrame.src = imeUrl;
+    document.body.appendChild(inputAppFrame);
+
+    SpecialPowers.pushPermissions([{
+      type: 'input',
+      allow: true,
+      context: {
+        url: imeUrl,
+        appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
+        isInBrowserElement: true
+      }
+    }], function() {
+      let mm = appFrameMM =
+        SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
+
+      inputAppFrame.addEventListener('mozbrowserloadend', function() {
+        mm.addMessageListener('test:appEvent', function(msg) {
+          ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
+        });
+        mm.addMessageListener('test:inputcontextchange', function(msg) {
+          nextStep && nextStep({ type: 'inputcontextchange' });
+        });
+        mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+        // Set the input app frame to be active
+        let req = inputAppFrame.setInputMethodActive(true);
+        resolve(req);
+      });
+    });
+  });
+}
+
+function setupContentFrame() {
+  let contentFrameScript = function contentFrameScript() {
+    let input = content.document.body.firstElementChild;
+
+    input.focus();
+  };
+
+  let iframe = document.createElement('iframe');
+  iframe.src = 'data:text/html,<html><body><input type="text"></body></html>';
+  iframe.setAttribute('mozbrowser', true);
+  document.body.appendChild(iframe);
+
+  let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+  iframe.addEventListener('mozbrowserloadend', function() {
+    mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false);
+  });
+}
+
+inputmethod_setup(function() {
+  Promise.resolve()
+    .then(() => setupTestRunner())
+    .then(() => setupContentFrame())
+    .then(() => setupInputAppFrame())
+    .catch((e) => {
+      ok(false, 'Error' + e.toString());
+      console.error(e);
+    });
+});
+
+</script>
+</pre>
+</body>
+</html>
+
--- a/dom/webidl/InputMethod.webidl
+++ b/dom/webidl/InputMethod.webidl
@@ -115,17 +115,17 @@ interface MozInputMethod : EventTarget {
 };
 
 /**
  * InputMethodManager contains a few of the global methods for the input app.
  */
 [JSImplementation="@mozilla.org/b2g-imm;1",
  Pref="dom.mozInputMethod.enabled",
  CheckAnyPermissions="input input-manage"]
-interface MozInputMethodManager {
+interface MozInputMethodManager : EventTarget {
   /**
    * 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();
 
@@ -160,16 +160,159 @@ interface MozInputMethodManager {
    * 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);
+
+  /**
+   * CustomEvent dispatches to System when there is an input to handle.
+   * If the API consumer failed to handle and call preventDefault(),
+   * there will be a message printed on the console.
+   *
+   * evt.detail is defined by MozInputContextFocusEventDetail.
+   */
+  [CheckAnyPermissions="input-manage"]
+  attribute EventHandler oninputcontextfocus;
+
+  /**
+   * Event dispatches to System when there is no longer an input to handle.
+   * If the API consumer failed to handle and call preventDefault(),
+   * there will be a message printed on the console.
+   */
+  [CheckAnyPermissions="input-manage"]
+  attribute EventHandler oninputcontextblur;
+
+  /**
+   * Event dispatches to System when there is a showAll() call.
+   * If the API consumer failed to handle and call preventDefault(),
+   * there will be a message printed on the console.
+   */
+  [CheckAnyPermissions="input-manage"]
+  attribute EventHandler onshowallrequest;
+
+  /**
+   * Event dispatches to System when there is a next() call.
+   * If the API consumer failed to handle and call preventDefault(),
+   * there will be a message printed on the console.
+   */
+  [CheckAnyPermissions="input-manage"]
+  attribute EventHandler onnextrequest;
+
+  /**
+   * Event dispatches to System when there is a addInput() call.
+   * The API consumer must call preventDefault() to indicate the event is
+   * consumed, otherwise the request is not considered handled even if
+   * waitUntil() was called.
+   *
+   * evt.detail is defined by MozInputRegistryEventDetail.
+   */
+  [CheckAnyPermissions="input-manage"]
+  attribute EventHandler onaddinputrequest;
+
+  /**
+   * Event dispatches to System when there is a removeInput() call.
+   * The API consumer must call preventDefault() to indicate the event is
+   * consumed, otherwise the request is not considered handled even if
+   * waitUntil() was called.
+   *
+   * evt.detail is defined by MozInputRegistryEventDetail.
+   */
+  [CheckAnyPermissions="input-manage"]
+  attribute EventHandler onremoveinputrequest;
+};
+
+/**
+ * Detail of the inputcontextfocus event.
+ */
+[JSImplementation="@mozilla.org/b2g-imm-focus;1",
+ Pref="dom.mozInputMethod.enabled",
+ CheckAnyPermissions="input-manage"]
+interface MozInputContextFocusEventDetail {
+  /**
+   * The type of the focused input.
+   */
+  readonly attribute MozInputMethodInputContextTypes type;
+  /**
+   * The input type of the focused input.
+   */
+  readonly attribute MozInputMethodInputContextInputTypes inputType;
+
+  /**
+   * The following is only needed for rendering and handling "option" input types,
+   * in System app.
+   */
+
+  /**
+   * Current value of the input/select element.
+   */
+  readonly attribute DOMString? value;
+  /**
+   * An object representing all the <optgroup> and <option> elements
+   * in the <select> element.
+   */
+  [Pure, Cached, Frozen]
+  readonly attribute MozInputContextChoicesInfo? choices;
+  /**
+   * Max/min value of <input>
+   */
+  readonly attribute DOMString? min;
+  readonly attribute DOMString? max;
+};
+
+/**
+ * Information about the options within the <select> element.
+ */
+dictionary MozInputContextChoicesInfo {
+  boolean multiple;
+  sequence<MozInputMethodChoiceDict> choices;
+};
+
+/**
+ * Content the header (<optgroup>) or an option (<option>).
+ */
+dictionary MozInputMethodChoiceDict {
+  boolean group;
+  DOMString text;
+  boolean disabled;
+  boolean? inGroup;
+  boolean? selected;
+  long? optionIndex;
+};
+
+/**
+ * detail of addinputrequest or removeinputrequest event.
+ */
+[JSImplementation="@mozilla.org/b2g-imm-input-registry;1",
+ Pref="dom.mozInputMethod.enabled",
+ CheckAnyPermissions="input-manage"]
+interface MozInputRegistryEventDetail {
+  /**
+   * Manifest URL of the requesting app.
+   */
+  readonly attribute DOMString manifestURL;
+  /**
+   * ID of the input
+   */
+  readonly attribute DOMString inputId;
+  /**
+   * Input manifest of the input to add.
+   * Null for removeinputrequest event.
+   */
+  [Pure, Cached, Frozen]
+  readonly attribute MozInputMethodInputManifest? inputManifest;
+  /**
+   * Resolve or Reject the addInput() or removeInput() call when the passed
+   * promises are resolved.
+   */
+  [Throws]
+  void waitUntil(Promise<any> p);
 };
 
 /**
  * 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
@@ -392,29 +535,30 @@ dictionary CompositionClauseParameters {
   long length;
 };
 
 /**
  * Types are HTML tag names of the inputs that is explosed with InputContext,
  * *and* the special keyword "contenteditable" for contenteditable element.
  */
 enum MozInputMethodInputContextTypes {
-  "input", "textarea", "contenteditable"
+  "input", "textarea", "contenteditable",
   /**
-   * <select> is managed by the API but it's not exposed through InputContext
-   * yet.
+   * <select> is managed by the API but it is handled by the System app only,
+   * so this value is only accessible by System app from inputcontextfocus event.
    */
-  // "select"
+  "select"
 };
 
 /**
  * InputTypes of the input that InputContext is representing. The value
- * is inferred from the type attribute of input element.
+ * is inferred from the type attribute of element.
  *
  * See https://html.spec.whatwg.org/multipage/forms.html#states-of-the-type-attribute
+ * for types of HTMLInputElement.
  *
  * They are divided into groups -- an layout/input capable of handling one type
  * in the group is considered as capable of handling all of the types in the
  * same group.
  * The layout/input that could handle type "text" is considered as the fallback
  * if none of layout/input installed can handle a specific type.
  *
  * Groups and fallbacks is enforced in Gaia System app currently.
@@ -439,22 +583,25 @@ enum MozInputMethodInputContextInputType
   /**
    * Group "email"
    */
   "email",
   /**
    * Group "password".
    * An non-Latin alphabet layout/input should not be able to handle this type.
    */
-  "password"
+  "password",
   /**
-   * Group "option". These types are handled by System app itself currently, and
-   * not exposed and allowed to handled with input context.
+   * Group "option". These types are handled by System app itself currently, so
+   * no input app will be set to active for these input types.
+   * System app access these types from inputcontextfocus event.
+   * ("select-one" and "select-multiple" are valid HTMLSelectElement#type.)
    */
-  //"datetime", "date", "month", "week", "time", "datetime-local", "color",
+  "datetime", "date", "month", "week", "time", "datetime-local", "color",
+  "select-one", "select-multiple"
   /**
    * These types are ignored by the API even though they are valid
    * HTMLInputElement#type.
    */
   //"checkbox", "radio", "file", "submit", "image", "range", "reset", "button"
 };
 
 /**