/* 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() { return null; }); 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 = { _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();