exec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. /*
  2. *
  3. * Licensed to the Apache Software Foundation (ASF) under one
  4. * or more contributor license agreements. See the NOTICE file
  5. * distributed with this work for additional information
  6. * regarding copyright ownership. The ASF licenses this file
  7. * to you under the Apache License, Version 2.0 (the
  8. * "License"); you may not use this file except in compliance
  9. * with the License. You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing,
  14. * software distributed under the License is distributed on an
  15. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16. * KIND, either express or implied. See the License for the
  17. * specific language governing permissions and limitations
  18. * under the License.
  19. *
  20. */
  21. /**
  22. * Execute a cordova command. It is up to the native side whether this action
  23. * is synchronous or asynchronous. The native side can return:
  24. * Synchronous: PluginResult object as a JSON string
  25. * Asynchronous: Empty string ""
  26. * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
  27. * depending upon the result of the action.
  28. *
  29. * @param {Function} success The success callback
  30. * @param {Function} fail The fail callback
  31. * @param {String} service The name of the service to use
  32. * @param {String} action Action to be run in cordova
  33. * @param {String[]} [args] Zero or more arguments to pass to the method
  34. */
  35. var cordova = require('cordova'),
  36. nativeApiProvider = require('cordova/android/nativeapiprovider'),
  37. utils = require('cordova/utils'),
  38. base64 = require('cordova/base64'),
  39. channel = require('cordova/channel'),
  40. jsToNativeModes = {
  41. PROMPT: 0,
  42. JS_OBJECT: 1
  43. },
  44. nativeToJsModes = {
  45. // Polls for messages using the JS->Native bridge.
  46. POLLING: 0,
  47. // For LOAD_URL to be viable, it would need to have a work-around for
  48. // the bug where the soft-keyboard gets dismissed when a message is sent.
  49. LOAD_URL: 1,
  50. // For the ONLINE_EVENT to be viable, it would need to intercept all event
  51. // listeners (both through addEventListener and window.ononline) as well
  52. // as set the navigator property itself.
  53. ONLINE_EVENT: 2,
  54. EVAL_BRIDGE: 3
  55. },
  56. jsToNativeBridgeMode, // Set lazily.
  57. nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE,
  58. pollEnabled = false,
  59. bridgeSecret = -1;
  60. var messagesFromNative = [];
  61. var isProcessing = false;
  62. var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve();
  63. var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); };
  64. function androidExec(success, fail, service, action, args) {
  65. if (bridgeSecret < 0) {
  66. // If we ever catch this firing, we'll need to queue up exec()s
  67. // and fire them once we get a secret. For now, I don't think
  68. // it's possible for exec() to be called since plugins are parsed but
  69. // not run until until after onNativeReady.
  70. throw new Error('exec() called without bridgeSecret');
  71. }
  72. // Set default bridge modes if they have not already been set.
  73. // By default, we use the failsafe, since addJavascriptInterface breaks too often
  74. if (jsToNativeBridgeMode === undefined) {
  75. androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
  76. }
  77. // If args is not provided, default to an empty array
  78. args = args || [];
  79. // Process any ArrayBuffers in the args into a string.
  80. for (var i = 0; i < args.length; i++) {
  81. if (utils.typeName(args[i]) == 'ArrayBuffer') {
  82. args[i] = base64.fromArrayBuffer(args[i]);
  83. }
  84. }
  85. var callbackId = service + cordova.callbackId++,
  86. argsJson = JSON.stringify(args);
  87. if (success || fail) {
  88. cordova.callbacks[callbackId] = {success:success, fail:fail};
  89. }
  90. var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
  91. // If argsJson was received by Java as null, try again with the PROMPT bridge mode.
  92. // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
  93. if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {
  94. androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
  95. androidExec(success, fail, service, action, args);
  96. androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
  97. } else if (msgs) {
  98. messagesFromNative.push(msgs);
  99. // Always process async to avoid exceptions messing up stack.
  100. nextTick(processMessages);
  101. }
  102. }
  103. androidExec.init = function() {
  104. //CB-11828
  105. //This failsafe checks the version of Android and if it's Jellybean, it switches it to
  106. //using the Online Event bridge for communicating from Native to JS
  107. //
  108. //It's ugly, but it's necessary.
  109. var check = navigator.userAgent.toLowerCase().match(/android\s[0-9].[0-9]/);
  110. var version_code = check && check[0].match(/4.[0-3].*/);
  111. if (version_code != null && nativeToJsBridgeMode == nativeToJsModes.EVAL_BRIDGE) {
  112. nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT;
  113. }
  114. bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode);
  115. channel.onNativeReady.fire();
  116. };
  117. function pollOnceFromOnlineEvent() {
  118. pollOnce(true);
  119. }
  120. function pollOnce(opt_fromOnlineEvent) {
  121. if (bridgeSecret < 0) {
  122. // This can happen when the NativeToJsMessageQueue resets the online state on page transitions.
  123. // We know there's nothing to retrieve, so no need to poll.
  124. return;
  125. }
  126. var msgs = nativeApiProvider.get().retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent);
  127. if (msgs) {
  128. messagesFromNative.push(msgs);
  129. // Process sync since we know we're already top-of-stack.
  130. processMessages();
  131. }
  132. }
  133. function pollingTimerFunc() {
  134. if (pollEnabled) {
  135. pollOnce();
  136. setTimeout(pollingTimerFunc, 50);
  137. }
  138. }
  139. function hookOnlineApis() {
  140. function proxyEvent(e) {
  141. cordova.fireWindowEvent(e.type);
  142. }
  143. // The network module takes care of firing online and offline events.
  144. // It currently fires them only on document though, so we bridge them
  145. // to window here (while first listening for exec()-releated online/offline
  146. // events).
  147. window.addEventListener('online', pollOnceFromOnlineEvent, false);
  148. window.addEventListener('offline', pollOnceFromOnlineEvent, false);
  149. cordova.addWindowEventHandler('online');
  150. cordova.addWindowEventHandler('offline');
  151. document.addEventListener('online', proxyEvent, false);
  152. document.addEventListener('offline', proxyEvent, false);
  153. }
  154. hookOnlineApis();
  155. androidExec.jsToNativeModes = jsToNativeModes;
  156. androidExec.nativeToJsModes = nativeToJsModes;
  157. androidExec.setJsToNativeBridgeMode = function(mode) {
  158. if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
  159. mode = jsToNativeModes.PROMPT;
  160. }
  161. nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
  162. jsToNativeBridgeMode = mode;
  163. };
  164. androidExec.setNativeToJsBridgeMode = function(mode) {
  165. if (mode == nativeToJsBridgeMode) {
  166. return;
  167. }
  168. if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
  169. pollEnabled = false;
  170. }
  171. nativeToJsBridgeMode = mode;
  172. // Tell the native side to switch modes.
  173. // Otherwise, it will be set by androidExec.init()
  174. if (bridgeSecret >= 0) {
  175. nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode);
  176. }
  177. if (mode == nativeToJsModes.POLLING) {
  178. pollEnabled = true;
  179. setTimeout(pollingTimerFunc, 1);
  180. }
  181. };
  182. function buildPayload(payload, message) {
  183. var payloadKind = message.charAt(0);
  184. if (payloadKind == 's') {
  185. payload.push(message.slice(1));
  186. } else if (payloadKind == 't') {
  187. payload.push(true);
  188. } else if (payloadKind == 'f') {
  189. payload.push(false);
  190. } else if (payloadKind == 'N') {
  191. payload.push(null);
  192. } else if (payloadKind == 'n') {
  193. payload.push(+message.slice(1));
  194. } else if (payloadKind == 'A') {
  195. var data = message.slice(1);
  196. payload.push(base64.toArrayBuffer(data));
  197. } else if (payloadKind == 'S') {
  198. payload.push(window.atob(message.slice(1)));
  199. } else if (payloadKind == 'M') {
  200. var multipartMessages = message.slice(1);
  201. while (multipartMessages !== "") {
  202. var spaceIdx = multipartMessages.indexOf(' ');
  203. var msgLen = +multipartMessages.slice(0, spaceIdx);
  204. var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
  205. multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1);
  206. buildPayload(payload, multipartMessage);
  207. }
  208. } else {
  209. payload.push(JSON.parse(message));
  210. }
  211. }
  212. // Processes a single message, as encoded by NativeToJsMessageQueue.java.
  213. function processMessage(message) {
  214. var firstChar = message.charAt(0);
  215. if (firstChar == 'J') {
  216. // This is deprecated on the .java side. It doesn't work with CSP enabled.
  217. eval(message.slice(1));
  218. } else if (firstChar == 'S' || firstChar == 'F') {
  219. var success = firstChar == 'S';
  220. var keepCallback = message.charAt(1) == '1';
  221. var spaceIdx = message.indexOf(' ', 2);
  222. var status = +message.slice(2, spaceIdx);
  223. var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
  224. var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
  225. var payloadMessage = message.slice(nextSpaceIdx + 1);
  226. var payload = [];
  227. buildPayload(payload, payloadMessage);
  228. cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
  229. } else {
  230. console.log("processMessage failed: invalid message: " + JSON.stringify(message));
  231. }
  232. }
  233. function processMessages() {
  234. // Check for the reentrant case.
  235. if (isProcessing) {
  236. return;
  237. }
  238. if (messagesFromNative.length === 0) {
  239. return;
  240. }
  241. isProcessing = true;
  242. try {
  243. var msg = popMessageFromQueue();
  244. // The Java side can send a * message to indicate that it
  245. // still has messages waiting to be retrieved.
  246. if (msg == '*' && messagesFromNative.length === 0) {
  247. nextTick(pollOnce);
  248. return;
  249. }
  250. processMessage(msg);
  251. } finally {
  252. isProcessing = false;
  253. if (messagesFromNative.length > 0) {
  254. nextTick(processMessages);
  255. }
  256. }
  257. }
  258. function popMessageFromQueue() {
  259. var messageBatch = messagesFromNative.shift();
  260. if (messageBatch == '*') {
  261. return '*';
  262. }
  263. var spaceIdx = messageBatch.indexOf(' ');
  264. var msgLen = +messageBatch.slice(0, spaceIdx);
  265. var message = messageBatch.substr(spaceIdx + 1, msgLen);
  266. messageBatch = messageBatch.slice(spaceIdx + msgLen + 1);
  267. if (messageBatch) {
  268. messagesFromNative.unshift(messageBatch);
  269. }
  270. return message;
  271. }
  272. module.exports = androidExec;