/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ /** * Execute a cordova command. It is up to the native side whether this action * is synchronous or asynchronous. The native side can return: * Synchronous: PluginResult object as a JSON string * Asynchronous: Empty string "" * If async, the native side will cordova.callbackSuccess or cordova.callbackError, * depending upon the result of the action. * * @param {Function} success The success callback * @param {Function} fail The fail callback * @param {String} service The name of the service to use * @param {String} action Action to be run in cordova * @param {String[]} [args] Zero or more arguments to pass to the method */ var cordova = require('cordova'), nativeApiProvider = require('cordova/android/nativeapiprovider'), utils = require('cordova/utils'), base64 = require('cordova/base64'), channel = require('cordova/channel'), jsToNativeModes = { PROMPT: 0, JS_OBJECT: 1 }, nativeToJsModes = { // Polls for messages using the JS->Native bridge. POLLING: 0, // For LOAD_URL to be viable, it would need to have a work-around for // the bug where the soft-keyboard gets dismissed when a message is sent. LOAD_URL: 1, // For the ONLINE_EVENT to be viable, it would need to intercept all event // listeners (both through addEventListener and window.ononline) as well // as set the navigator property itself. ONLINE_EVENT: 2, EVAL_BRIDGE: 3 }, jsToNativeBridgeMode, // Set lazily. nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE, pollEnabled = false, bridgeSecret = -1; var messagesFromNative = []; var isProcessing = false; var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve(); var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); }; function androidExec(success, fail, service, action, args) { if (bridgeSecret < 0) { // If we ever catch this firing, we'll need to queue up exec()s // and fire them once we get a secret. For now, I don't think // it's possible for exec() to be called since plugins are parsed but // not run until until after onNativeReady. throw new Error('exec() called without bridgeSecret'); } // Set default bridge modes if they have not already been set. // By default, we use the failsafe, since addJavascriptInterface breaks too often if (jsToNativeBridgeMode === undefined) { androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); } // If args is not provided, default to an empty array args = args || []; // Process any ArrayBuffers in the args into a string. for (var i = 0; i < args.length; i++) { if (utils.typeName(args[i]) == 'ArrayBuffer') { args[i] = base64.fromArrayBuffer(args[i]); } } var callbackId = service + cordova.callbackId++, argsJson = JSON.stringify(args); if (success || fail) { cordova.callbacks[callbackId] = {success:success, fail:fail}; } var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson); // If argsJson was received by Java as null, try again with the PROMPT bridge mode. // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666. if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") { androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT); androidExec(success, fail, service, action, args); androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); } else if (msgs) { messagesFromNative.push(msgs); // Always process async to avoid exceptions messing up stack. nextTick(processMessages); } } androidExec.init = function() { bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode); channel.onNativeReady.fire(); }; function pollOnceFromOnlineEvent() { pollOnce(true); } function pollOnce(opt_fromOnlineEvent) { if (bridgeSecret < 0) { // This can happen when the NativeToJsMessageQueue resets the online state on page transitions. // We know there's nothing to retrieve, so no need to poll. return; } var msgs = nativeApiProvider.get().retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent); if (msgs) { messagesFromNative.push(msgs); // Process sync since we know we're already top-of-stack. processMessages(); } } function pollingTimerFunc() { if (pollEnabled) { pollOnce(); setTimeout(pollingTimerFunc, 50); } } function hookOnlineApis() { function proxyEvent(e) { cordova.fireWindowEvent(e.type); } // The network module takes care of firing online and offline events. // It currently fires them only on document though, so we bridge them // to window here (while first listening for exec()-releated online/offline // events). window.addEventListener('online', pollOnceFromOnlineEvent, false); window.addEventListener('offline', pollOnceFromOnlineEvent, false); cordova.addWindowEventHandler('online'); cordova.addWindowEventHandler('offline'); document.addEventListener('online', proxyEvent, false); document.addEventListener('offline', proxyEvent, false); } hookOnlineApis(); androidExec.jsToNativeModes = jsToNativeModes; androidExec.nativeToJsModes = nativeToJsModes; androidExec.setJsToNativeBridgeMode = function(mode) { if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) { mode = jsToNativeModes.PROMPT; } nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT); jsToNativeBridgeMode = mode; }; androidExec.setNativeToJsBridgeMode = function(mode) { if (mode == nativeToJsBridgeMode) { return; } if (nativeToJsBridgeMode == nativeToJsModes.POLLING) { pollEnabled = false; } nativeToJsBridgeMode = mode; // Tell the native side to switch modes. // Otherwise, it will be set by androidExec.init() if (bridgeSecret >= 0) { nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode); } if (mode == nativeToJsModes.POLLING) { pollEnabled = true; setTimeout(pollingTimerFunc, 1); } }; function buildPayload(payload, message) { var payloadKind = message.charAt(0); if (payloadKind == 's') { payload.push(message.slice(1)); } else if (payloadKind == 't') { payload.push(true); } else if (payloadKind == 'f') { payload.push(false); } else if (payloadKind == 'N') { payload.push(null); } else if (payloadKind == 'n') { payload.push(+message.slice(1)); } else if (payloadKind == 'A') { var data = message.slice(1); payload.push(base64.toArrayBuffer(data)); } else if (payloadKind == 'S') { payload.push(window.atob(message.slice(1))); } else if (payloadKind == 'M') { var multipartMessages = message.slice(1); while (multipartMessages !== "") { var spaceIdx = multipartMessages.indexOf(' '); var msgLen = +multipartMessages.slice(0, spaceIdx); var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen); multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1); buildPayload(payload, multipartMessage); } } else { payload.push(JSON.parse(message)); } } // Processes a single message, as encoded by NativeToJsMessageQueue.java. function processMessage(message) { var firstChar = message.charAt(0); if (firstChar == 'J') { // This is deprecated on the .java side. It doesn't work with CSP enabled. eval(message.slice(1)); } else if (firstChar == 'S' || firstChar == 'F') { var success = firstChar == 'S'; var keepCallback = message.charAt(1) == '1'; var spaceIdx = message.indexOf(' ', 2); var status = +message.slice(2, spaceIdx); var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1); var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx); var payloadMessage = message.slice(nextSpaceIdx + 1); var payload = []; buildPayload(payload, payloadMessage); cordova.callbackFromNative(callbackId, success, status, payload, keepCallback); } else { console.log("processMessage failed: invalid message: " + JSON.stringify(message)); } } function processMessages() { // Check for the reentrant case. if (isProcessing) { return; } if (messagesFromNative.length === 0) { return; } isProcessing = true; try { var msg = popMessageFromQueue(); // The Java side can send a * message to indicate that it // still has messages waiting to be retrieved. if (msg == '*' && messagesFromNative.length === 0) { nextTick(pollOnce); return; } processMessage(msg); } finally { isProcessing = false; if (messagesFromNative.length > 0) { nextTick(processMessages); } } } function popMessageFromQueue() { var messageBatch = messagesFromNative.shift(); if (messageBatch == '*') { return '*'; } var spaceIdx = messageBatch.indexOf(' '); var msgLen = +messageBatch.slice(0, spaceIdx); var message = messageBatch.substr(spaceIdx + 1, msgLen); messageBatch = messageBatch.slice(spaceIdx + msgLen + 1); if (messageBatch) { messagesFromNative.unshift(messageBatch); } return message; } module.exports = androidExec;