Keyboard.jsm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. 'use strict';
  5. this.EXPORTED_SYMBOLS = ['Keyboard'];
  6. const Cu = Components.utils;
  7. const Cc = Components.classes;
  8. const Ci = Components.interfaces;
  9. Cu.import('resource://gre/modules/Services.jsm');
  10. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  11. XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
  12. "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
  13. XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
  14. "resource://gre/modules/SystemAppProxy.jsm");
  15. XPCOMUtils.defineLazyGetter(this, "appsService", function() {
  16. return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
  17. });
  18. XPCOMUtils.defineLazyGetter(this, "hardwareKeyHandler", function() {
  19. return null;
  20. });
  21. var Utils = {
  22. getMMFromMessage: function u_getMMFromMessage(msg) {
  23. let mm;
  24. try {
  25. mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
  26. .frameLoader.messageManager;
  27. } catch(e) {
  28. mm = msg.target;
  29. }
  30. return mm;
  31. },
  32. checkPermissionForMM: function u_checkPermissionForMM(mm, permName) {
  33. return mm.assertPermission(permName);
  34. }
  35. };
  36. this.Keyboard = {
  37. _isConnectedToHardwareKeyHandler: false,
  38. _formMM: null, // The current web page message manager.
  39. _keyboardMM: null, // The keyboard app message manager.
  40. _keyboardID: -1, // The keyboard app's ID number. -1 = invalid
  41. _nextKeyboardID: 0, // The ID number counter.
  42. _systemMMs: [], // The message managers registered to handle system async
  43. // messages.
  44. _supportsSwitchingTypes: [],
  45. _systemMessageNames: [
  46. 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
  47. 'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister'
  48. ],
  49. _messageNames: [
  50. 'RemoveFocus',
  51. 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
  52. 'SwitchToNextInputMethod', 'HideInputMethod',
  53. 'SendKey', 'GetContext',
  54. 'SetComposition', 'EndComposition',
  55. 'RegisterSync', 'Unregister',
  56. 'ReplyHardwareKeyEvent'
  57. ],
  58. get formMM() {
  59. if (this._formMM && !Cu.isDeadWrapper(this._formMM))
  60. return this._formMM;
  61. return null;
  62. },
  63. set formMM(mm) {
  64. this._formMM = mm;
  65. },
  66. sendToForm: function(name, data) {
  67. if (!this.formMM) {
  68. dump("Keyboard.jsm: Attempt to send message " + name +
  69. " to form but no message manager exists.\n");
  70. return;
  71. }
  72. try {
  73. this.formMM.sendAsyncMessage(name, data);
  74. } catch(e) { }
  75. },
  76. sendToKeyboard: function(name, data) {
  77. try {
  78. this._keyboardMM.sendAsyncMessage(name, data);
  79. } catch(e) {
  80. return false;
  81. }
  82. return true;
  83. },
  84. sendToSystem: function(name, data) {
  85. if (!this._systemMMs.length) {
  86. dump("Keyboard.jsm: Attempt to send message " + name +
  87. " to system but no message manager registered.\n");
  88. return;
  89. }
  90. this._systemMMs.forEach((mm, i) => {
  91. data.inputManageId = i;
  92. mm.sendAsyncMessage(name, data);
  93. });
  94. },
  95. init: function keyboardInit() {
  96. Services.obs.addObserver(this, 'inprocess-browser-shown', false);
  97. Services.obs.addObserver(this, 'remote-browser-shown', false);
  98. Services.obs.addObserver(this, 'oop-frameloader-crashed', false);
  99. Services.obs.addObserver(this, 'message-manager-close', false);
  100. // For receiving the native hardware keyboard event
  101. if (hardwareKeyHandler) {
  102. hardwareKeyHandler.registerListener(this);
  103. }
  104. for (let name of this._messageNames) {
  105. ppmm.addMessageListener('Keyboard:' + name, this);
  106. }
  107. for (let name of this._systemMessageNames) {
  108. ppmm.addMessageListener('System:' + name, this);
  109. }
  110. this.inputRegistryGlue = new InputRegistryGlue();
  111. },
  112. // This method will be registered into nsIHardwareKeyHandler:
  113. // Send the initialized dictionary retrieved from the native keyboard event
  114. // to input-method-app for generating a new event.
  115. onHardwareKey: function onHardwareKeyReceived(evt) {
  116. return this.sendToKeyboard('Keyboard:ReceiveHardwareKeyEvent', {
  117. type: evt.type,
  118. keyDict: evt.initDict
  119. });
  120. },
  121. observe: function keyboardObserve(subject, topic, data) {
  122. let frameLoader = null;
  123. let mm = null;
  124. if (topic == 'message-manager-close') {
  125. mm = subject;
  126. } else {
  127. frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
  128. mm = frameLoader.messageManager;
  129. }
  130. if (topic == 'oop-frameloader-crashed' ||
  131. topic == 'message-manager-close') {
  132. if (this.formMM == mm) {
  133. // The application has been closed unexpectingly. Let's tell the
  134. // keyboard app that the focus has been lost.
  135. this.sendToKeyboard('Keyboard:Blur', {});
  136. // Notify system app to hide keyboard.
  137. this.sendToSystem('System:Blur', {});
  138. // XXX: To be removed when content migrate away from mozChromeEvents.
  139. SystemAppProxy.dispatchEvent({
  140. type: 'inputmethod-contextchange',
  141. inputType: 'blur'
  142. });
  143. this.formMM = null;
  144. }
  145. } else {
  146. // Ignore notifications that aren't from a BrowserOrApp
  147. if (!frameLoader.ownerIsMozBrowserOrAppFrame) {
  148. return;
  149. }
  150. this.initFormsFrameScript(mm);
  151. }
  152. },
  153. initFormsFrameScript: function(mm) {
  154. mm.addMessageListener('Forms:Focus', this);
  155. mm.addMessageListener('Forms:Blur', this);
  156. mm.addMessageListener('Forms:SelectionChange', this);
  157. mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
  158. mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this);
  159. mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
  160. mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this);
  161. mm.addMessageListener('Forms:SendKey:Result:OK', this);
  162. mm.addMessageListener('Forms:SendKey:Result:Error', this);
  163. mm.addMessageListener('Forms:SequenceError', this);
  164. mm.addMessageListener('Forms:GetContext:Result:OK', this);
  165. mm.addMessageListener('Forms:SetComposition:Result:OK', this);
  166. mm.addMessageListener('Forms:EndComposition:Result:OK', this);
  167. },
  168. receiveMessage: function keyboardReceiveMessage(msg) {
  169. // If we get a 'Keyboard:XXX'/'System:XXX' message, check that the sender
  170. // has the required permission.
  171. let mm;
  172. // Assert the permission based on the prefix of the message.
  173. let permName;
  174. if (msg.name.startsWith("Keyboard:")) {
  175. permName = "input";
  176. } else if (msg.name.startsWith("System:")) {
  177. permName = "input-manage";
  178. }
  179. // There is no permission to check (nor we need to get the mm)
  180. // for Form: messages.
  181. if (permName) {
  182. mm = Utils.getMMFromMessage(msg);
  183. if (!mm) {
  184. dump("Keyboard.jsm: Message " + msg.name + " has no message manager.");
  185. return;
  186. }
  187. if (!Utils.checkPermissionForMM(mm, permName)) {
  188. dump("Keyboard.jsm: Message " + msg.name +
  189. " from a content process with no '" + permName + "' privileges.\n");
  190. return;
  191. }
  192. }
  193. // we don't process kb messages (other than register)
  194. // if they come from a kb that we're currently not regsitered for.
  195. // this decision is made with the kbID kept by us and kb app
  196. let kbID = null;
  197. if ('kbID' in msg.data) {
  198. kbID = msg.data.kbID;
  199. }
  200. if (0 === msg.name.indexOf('Keyboard:') &&
  201. ('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID)
  202. ) {
  203. return;
  204. }
  205. switch (msg.name) {
  206. case 'Forms:Focus':
  207. this.handleFocus(msg);
  208. break;
  209. case 'Forms:Blur':
  210. this.handleBlur(msg);
  211. break;
  212. case 'Forms:SelectionChange':
  213. case 'Forms:SetSelectionRange:Result:OK':
  214. case 'Forms:ReplaceSurroundingText:Result:OK':
  215. case 'Forms:SendKey:Result:OK':
  216. case 'Forms:SendKey:Result:Error':
  217. case 'Forms:SequenceError':
  218. case 'Forms:GetContext:Result:OK':
  219. case 'Forms:SetComposition:Result:OK':
  220. case 'Forms:EndComposition:Result:OK':
  221. case 'Forms:SetSelectionRange:Result:Error':
  222. case 'Forms:ReplaceSurroundingText:Result:Error':
  223. let name = msg.name.replace(/^Forms/, 'Keyboard');
  224. this.forwardEvent(name, msg);
  225. break;
  226. case 'System:SetValue':
  227. this.setValue(msg);
  228. break;
  229. case 'Keyboard:RemoveFocus':
  230. case 'System:RemoveFocus':
  231. this.removeFocus();
  232. break;
  233. case 'System:RegisterSync': {
  234. if (this._systemMMs.length !== 0) {
  235. dump('Keyboard.jsm Warning: There are more than one content page ' +
  236. 'with input-manage permission. There will be undeterministic ' +
  237. 'responses to addInput()/removeInput() if both content pages are ' +
  238. 'trying to respond to the same request event.\n');
  239. }
  240. let id = this._systemMMs.length;
  241. this._systemMMs.push(mm);
  242. return id;
  243. }
  244. case 'System:Unregister':
  245. this._systemMMs.splice(msg.data.id, 1);
  246. break;
  247. case 'System:SetSelectedOption':
  248. this.setSelectedOption(msg);
  249. break;
  250. case 'System:SetSelectedOptions':
  251. this.setSelectedOption(msg);
  252. break;
  253. case 'System:SetSupportsSwitchingTypes':
  254. this.setSupportsSwitchingTypes(msg);
  255. break;
  256. case 'Keyboard:SetSelectionRange':
  257. this.setSelectionRange(msg);
  258. break;
  259. case 'Keyboard:ReplaceSurroundingText':
  260. this.replaceSurroundingText(msg);
  261. break;
  262. case 'Keyboard:SwitchToNextInputMethod':
  263. this.switchToNextInputMethod();
  264. break;
  265. case 'Keyboard:ShowInputMethodPicker':
  266. this.showInputMethodPicker();
  267. break;
  268. case 'Keyboard:SendKey':
  269. this.sendKey(msg);
  270. break;
  271. case 'Keyboard:GetContext':
  272. this.getContext(msg);
  273. break;
  274. case 'Keyboard:SetComposition':
  275. this.setComposition(msg);
  276. break;
  277. case 'Keyboard:EndComposition':
  278. this.endComposition(msg);
  279. break;
  280. case 'Keyboard:RegisterSync':
  281. this._keyboardMM = mm;
  282. if (kbID) {
  283. // keyboard identifies itself, use its kbID
  284. // this msg would be async, so no need to return
  285. this._keyboardID = kbID;
  286. }else{
  287. // generate the id for the keyboard
  288. this._keyboardID = this._nextKeyboardID;
  289. this._nextKeyboardID++;
  290. // this msg is sync,
  291. // and we want to return the id back to inputmethod
  292. return this._keyboardID;
  293. }
  294. break;
  295. case 'Keyboard:Unregister':
  296. this._keyboardMM = null;
  297. this._keyboardID = -1;
  298. break;
  299. case 'Keyboard:ReplyHardwareKeyEvent':
  300. if (hardwareKeyHandler) {
  301. let reply = msg.data;
  302. hardwareKeyHandler.onHandledByInputMethodApp(reply.type,
  303. reply.defaultPrevented);
  304. }
  305. break;
  306. }
  307. },
  308. handleFocus: function keyboardHandleFocus(msg) {
  309. // Set the formMM to the new message manager received.
  310. let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
  311. .frameLoader.messageManager;
  312. this.formMM = mm;
  313. // Notify the nsIHardwareKeyHandler that the input-method-app is active now.
  314. if (hardwareKeyHandler && !this._isConnectedToHardwareKeyHandler) {
  315. this._isConnectedToHardwareKeyHandler = true;
  316. hardwareKeyHandler.onInputMethodAppConnected();
  317. }
  318. // Notify the current active input app to gain focus.
  319. this.forwardEvent('Keyboard:Focus', msg);
  320. // Notify System app, used also to render value selectors for now;
  321. // that's why we need the info about choices / min / max here as well...
  322. this.sendToSystem('System:Focus', msg.data);
  323. // XXX: To be removed when content migrate away from mozChromeEvents.
  324. SystemAppProxy.dispatchEvent({
  325. type: 'inputmethod-contextchange',
  326. inputType: msg.data.inputType,
  327. value: msg.data.value,
  328. choices: JSON.stringify(msg.data.choices),
  329. min: msg.data.min,
  330. max: msg.data.max
  331. });
  332. },
  333. handleBlur: function keyboardHandleBlur(msg) {
  334. let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
  335. .frameLoader.messageManager;
  336. // A blur message can't be sent to the keyboard if the focus has
  337. // already been taken away at first place.
  338. // This check is here to prevent problem caused by out-of-order
  339. // ipc messages from two processes.
  340. if (mm !== this.formMM) {
  341. return;
  342. }
  343. // unset formMM
  344. this.formMM = null;
  345. // Notify the nsIHardwareKeyHandler that
  346. // the input-method-app is disabled now.
  347. if (hardwareKeyHandler && this._isConnectedToHardwareKeyHandler) {
  348. this._isConnectedToHardwareKeyHandler = false;
  349. hardwareKeyHandler.onInputMethodAppDisconnected();
  350. }
  351. this.forwardEvent('Keyboard:Blur', msg);
  352. this.sendToSystem('System:Blur', {});
  353. // XXX: To be removed when content migrate away from mozChromeEvents.
  354. SystemAppProxy.dispatchEvent({
  355. type: 'inputmethod-contextchange',
  356. inputType: 'blur'
  357. });
  358. },
  359. forwardEvent: function keyboardForwardEvent(newEventName, msg) {
  360. this.sendToKeyboard(newEventName, msg.data);
  361. },
  362. setSelectedOption: function keyboardSetSelectedOption(msg) {
  363. this.sendToForm('Forms:Select:Choice', msg.data);
  364. },
  365. setSelectedOptions: function keyboardSetSelectedOptions(msg) {
  366. this.sendToForm('Forms:Select:Choice', msg.data);
  367. },
  368. setSelectionRange: function keyboardSetSelectionRange(msg) {
  369. this.sendToForm('Forms:SetSelectionRange', msg.data);
  370. },
  371. setValue: function keyboardSetValue(msg) {
  372. this.sendToForm('Forms:Input:Value', msg.data);
  373. },
  374. removeFocus: function keyboardRemoveFocus() {
  375. if (!this.formMM) {
  376. return;
  377. }
  378. this.sendToForm('Forms:Select:Blur', {});
  379. },
  380. replaceSurroundingText: function keyboardReplaceSurroundingText(msg) {
  381. this.sendToForm('Forms:ReplaceSurroundingText', msg.data);
  382. },
  383. showInputMethodPicker: function keyboardShowInputMethodPicker() {
  384. this.sendToSystem('System:ShowAll', {});
  385. // XXX: To be removed with mozContentEvent support from shell.js
  386. SystemAppProxy.dispatchEvent({
  387. type: "inputmethod-showall"
  388. });
  389. },
  390. switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
  391. this.sendToSystem('System:Next', {});
  392. // XXX: To be removed with mozContentEvent support from shell.js
  393. SystemAppProxy.dispatchEvent({
  394. type: "inputmethod-next"
  395. });
  396. },
  397. sendKey: function keyboardSendKey(msg) {
  398. this.sendToForm('Forms:Input:SendKey', msg.data);
  399. },
  400. getContext: function keyboardGetContext(msg) {
  401. if (!this.formMM) {
  402. return;
  403. }
  404. this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
  405. types: this._supportsSwitchingTypes
  406. });
  407. this.sendToForm('Forms:GetContext', msg.data);
  408. },
  409. setComposition: function keyboardSetComposition(msg) {
  410. this.sendToForm('Forms:SetComposition', msg.data);
  411. },
  412. endComposition: function keyboardEndComposition(msg) {
  413. this.sendToForm('Forms:EndComposition', msg.data);
  414. },
  415. setSupportsSwitchingTypes: function setSupportsSwitchingTypes(msg) {
  416. this._supportsSwitchingTypes = msg.data.types;
  417. this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', msg.data);
  418. },
  419. // XXX: To be removed with mozContentEvent support from shell.js
  420. setLayouts: function keyboardSetLayouts(layouts) {
  421. // The input method plugins may not have loaded yet,
  422. // cache the layouts so on init we can respond immediately instead
  423. // of going back and forth between keyboard_manager
  424. var types = [];
  425. Object.keys(layouts).forEach((type) => {
  426. if (layouts[type] > 1) {
  427. types.push(type);
  428. }
  429. });
  430. this._supportsSwitchingTypes = types;
  431. this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
  432. types: types
  433. });
  434. }
  435. };
  436. function InputRegistryGlue() {
  437. this._messageId = 0;
  438. this._msgMap = new Map();
  439. ppmm.addMessageListener('InputRegistry:Add', this);
  440. ppmm.addMessageListener('InputRegistry:Remove', this);
  441. ppmm.addMessageListener('System:InputRegistry:Add:Done', this);
  442. ppmm.addMessageListener('System:InputRegistry:Remove:Done', this);
  443. };
  444. InputRegistryGlue.prototype.receiveMessage = function(msg) {
  445. let mm = Utils.getMMFromMessage(msg);
  446. let permName = msg.name.startsWith("System:") ? "input-mgmt" : "input";
  447. if (!Utils.checkPermissionForMM(mm, permName)) {
  448. dump("InputRegistryGlue message " + msg.name +
  449. " from a content process with no " + permName + " privileges.");
  450. return;
  451. }
  452. switch (msg.name) {
  453. case 'InputRegistry:Add':
  454. this.addInput(msg, mm);
  455. break;
  456. case 'InputRegistry:Remove':
  457. this.removeInput(msg, mm);
  458. break;
  459. case 'System:InputRegistry:Add:Done':
  460. case 'System:InputRegistry:Remove:Done':
  461. this.returnMessage(msg.data);
  462. break;
  463. }
  464. };
  465. InputRegistryGlue.prototype.addInput = function(msg, mm) {
  466. let msgId = this._messageId++;
  467. this._msgMap.set(msgId, {
  468. mm: mm,
  469. requestId: msg.data.requestId
  470. });
  471. let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
  472. Keyboard.sendToSystem('System:InputRegistry:Add', {
  473. id: msgId,
  474. manifestURL: manifestURL,
  475. inputId: msg.data.inputId,
  476. inputManifest: msg.data.inputManifest
  477. });
  478. // XXX: To be removed when content migrate away from mozChromeEvents.
  479. SystemAppProxy.dispatchEvent({
  480. type: 'inputregistry-add',
  481. id: msgId,
  482. manifestURL: manifestURL,
  483. inputId: msg.data.inputId,
  484. inputManifest: msg.data.inputManifest
  485. });
  486. };
  487. InputRegistryGlue.prototype.removeInput = function(msg, mm) {
  488. let msgId = this._messageId++;
  489. this._msgMap.set(msgId, {
  490. mm: mm,
  491. requestId: msg.data.requestId
  492. });
  493. let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
  494. Keyboard.sendToSystem('System:InputRegistry:Remove', {
  495. id: msgId,
  496. manifestURL: manifestURL,
  497. inputId: msg.data.inputId
  498. });
  499. // XXX: To be removed when content migrate away from mozChromeEvents.
  500. SystemAppProxy.dispatchEvent({
  501. type: 'inputregistry-remove',
  502. id: msgId,
  503. manifestURL: manifestURL,
  504. inputId: msg.data.inputId
  505. });
  506. };
  507. InputRegistryGlue.prototype.returnMessage = function(detail) {
  508. if (!this._msgMap.has(detail.id)) {
  509. dump('InputRegistryGlue: Ignoring already handled message response. ' +
  510. 'id=' + detail.id + '\n');
  511. return;
  512. }
  513. let { mm, requestId } = this._msgMap.get(detail.id);
  514. this._msgMap.delete(detail.id);
  515. if (Cu.isDeadWrapper(mm)) {
  516. dump('InputRegistryGlue: Message manager has already died.\n');
  517. return;
  518. }
  519. if (!('error' in detail)) {
  520. mm.sendAsyncMessage('InputRegistry:Result:OK', {
  521. requestId: requestId
  522. });
  523. } else {
  524. mm.sendAsyncMessage('InputRegistry:Result:Error', {
  525. error: detail.error,
  526. requestId: requestId
  527. });
  528. }
  529. };
  530. this.Keyboard.init();