diff options
Diffstat (limited to 'dom/inputmethod/Keyboard.jsm')
-rw-r--r-- | dom/inputmethod/Keyboard.jsm | 644 |
1 files changed, 644 insertions, 0 deletions
diff --git a/dom/inputmethod/Keyboard.jsm b/dom/inputmethod/Keyboard.jsm new file mode 100644 index 000000000..22f87ffbc --- /dev/null +++ b/dom/inputmethod/Keyboard.jsm @@ -0,0 +1,644 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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'; + +this.EXPORTED_SYMBOLS = ['Keyboard']; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); + +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; + } + + 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: [ + 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions', + 'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister' + ], + + _messageNames: [ + 'RemoveFocus', + 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker', + 'SwitchToNextInputMethod', 'HideInputMethod', + 'SendKey', 'GetContext', + 'SetComposition', 'EndComposition', + 'RegisterSync', 'Unregister', + 'ReplyHardwareKeyEvent' + ], + + get formMM() { + if (this._formMM && !Cu.isDeadWrapper(this._formMM)) + return this._formMM; + + 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); + } 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; + } + + 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 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); + mm = frameLoader.messageManager; + } + + 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.ownerIsMozBrowserOrAppFrame) { + return; + } + this.initFormsFrameScript(mm); + } + }, + + initFormsFrameScript: function(mm) { + mm.addMessageListener('Forms:Focus', this); + mm.addMessageListener('Forms:Blur', this); + mm.addMessageListener('Forms:SelectionChange', this); + mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this); + mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this); + mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this); + mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this); + mm.addMessageListener('Forms:SendKey:Result:OK', this); + mm.addMessageListener('Forms:SendKey:Result:Error', this); + mm.addMessageListener('Forms:SequenceError', this); + mm.addMessageListener('Forms:GetContext:Result:OK', this); + 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; + + // 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; + } + if (!Utils.checkPermissionForMM(mm, permName)) { + dump("Keyboard.jsm: Message " + msg.name + + " from a content process with no '" + permName + "' privileges.\n"); + 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; + if ('kbID' in msg.data) { + kbID = msg.data.kbID; + } + + if (0 === msg.name.indexOf('Keyboard:') && + ('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID) + ) { + return; + } + + switch (msg.name) { + case 'Forms:Focus': + this.handleFocus(msg); + break; + case 'Forms:Blur': + this.handleBlur(msg); + break; + case 'Forms:SelectionChange': + case 'Forms:SetSelectionRange:Result:OK': + case 'Forms:ReplaceSurroundingText:Result:OK': + case 'Forms:SendKey:Result:OK': + case 'Forms:SendKey:Result:Error': + case 'Forms:SequenceError': + case 'Forms:GetContext:Result:OK': + case 'Forms:SetComposition:Result:OK': + case 'Forms:EndComposition:Result:OK': + case 'Forms:SetSelectionRange:Result:Error': + case 'Forms:ReplaceSurroundingText:Result:Error': + let name = msg.name.replace(/^Forms/, 'Keyboard'); + this.forwardEvent(name, msg); + break; + + 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); + break; + case 'Keyboard:SetSelectionRange': + this.setSelectionRange(msg); + break; + case 'Keyboard:ReplaceSurroundingText': + this.replaceSurroundingText(msg); + break; + case 'Keyboard:SwitchToNextInputMethod': + this.switchToNextInputMethod(); + break; + case 'Keyboard:ShowInputMethodPicker': + this.showInputMethodPicker(); + break; + case 'Keyboard:SendKey': + this.sendKey(msg); + break; + case 'Keyboard:GetContext': + this.getContext(msg); + break; + case 'Keyboard:SetComposition': + this.setComposition(msg); + break; + case 'Keyboard:EndComposition': + this.endComposition(msg); + break; + 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; + this._nextKeyboardID++; + // this msg is sync, + // 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. + 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 + }); + }, + + handleBlur: function keyboardHandleBlur(msg) { + let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader.messageManager; + // A blur message can't be sent to the keyboard if the focus has + // already been taken away at first place. + // This check is here to prevent problem caused by out-of-order + // 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' + }); + }, + + forwardEvent: function keyboardForwardEvent(newEventName, msg) { + this.sendToKeyboard(newEventName, msg.data); + }, + + setSelectedOption: function keyboardSetSelectedOption(msg) { + this.sendToForm('Forms:Select:Choice', msg.data); + }, + + setSelectedOptions: function keyboardSetSelectedOptions(msg) { + this.sendToForm('Forms:Select:Choice', msg.data); + }, + + setSelectionRange: function keyboardSetSelectionRange(msg) { + 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() { + 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" + }); + }, + + sendKey: function keyboardSendKey(msg) { + this.sendToForm('Forms:Input:SendKey', msg.data); + }, + + getContext: function keyboardGetContext(msg) { + 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); + }, + + 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 + var types = []; + + 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); + 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); + + 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 " + 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 + }); +}; + +InputRegistryGlue.prototype.removeInput = 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: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 { + mm.sendAsyncMessage('InputRegistry:Result:Error', { + error: detail.error, + requestId: requestId + }); + } +}; + +this.Keyboard.init(); |