| /* |
| * |
| * 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'), |
| jsToNativeModes = { |
| PROMPT: 0, |
| JS_OBJECT: 1, |
| // This mode is currently for benchmarking purposes only. It must be enabled |
| // on the native side through the ENABLE_LOCATION_CHANGE_EXEC_MODE |
| // constant within CordovaWebViewClient.java before it will work. |
| LOCATION_CHANGE: 2 |
| }, |
| 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, |
| // Uses reflection to access private APIs of the WebView that can send JS |
| // to be executed. |
| // Requires Android 3.2.4 or above. |
| PRIVATE_API: 3 |
| }, |
| jsToNativeBridgeMode, // Set lazily. |
| nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT, |
| pollEnabled = false, |
| messagesFromNative = []; |
| |
| function androidExec(success, fail, service, action, args) { |
| // 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); |
| } |
| |
| // 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}; |
| } |
| |
| if (jsToNativeBridgeMode == jsToNativeModes.LOCATION_CHANGE) { |
| window.location = 'http://cdv_exec/' + service + '#' + action + '#' + callbackId + '#' + argsJson; |
| } else { |
| var messages = nativeApiProvider.get().exec(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 && messages === "@Null arguments.") { |
| androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT); |
| androidExec(success, fail, service, action, args); |
| androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); |
| return; |
| } else { |
| androidExec.processMessages(messages); |
| } |
| } |
| } |
| |
| function pollOnceFromOnlineEvent() { |
| pollOnce(true); |
| } |
| |
| function pollOnce(opt_fromOnlineEvent) { |
| var msg = nativeApiProvider.get().retrieveJsMessages(!!opt_fromOnlineEvent); |
| androidExec.processMessages(msg); |
| } |
| |
| 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) { |
| console.log('Falling back on PROMPT mode since _cordovaNative is missing. Expected for Android 3.2 and lower only.'); |
| 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. |
| nativeApiProvider.get().setNativeToJsBridgeMode(mode); |
| |
| if (mode == nativeToJsModes.POLLING) { |
| pollEnabled = true; |
| setTimeout(pollingTimerFunc, 1); |
| } |
| }; |
| |
| // Processes a single message, as encoded by NativeToJsMessageQueue.java. |
| function processMessage(message) { |
| try { |
| var firstChar = message.charAt(0); |
| if (firstChar == 'J') { |
| 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 payloadKind = message.charAt(nextSpaceIdx + 1); |
| var payload; |
| if (payloadKind == 's') { |
| payload = message.slice(nextSpaceIdx + 2); |
| } else if (payloadKind == 't') { |
| payload = true; |
| } else if (payloadKind == 'f') { |
| payload = false; |
| } else if (payloadKind == 'N') { |
| payload = null; |
| } else if (payloadKind == 'n') { |
| payload = +message.slice(nextSpaceIdx + 2); |
| } else if (payloadKind == 'A') { |
| var data = message.slice(nextSpaceIdx + 2); |
| var bytes = window.atob(data); |
| var arraybuffer = new Uint8Array(bytes.length); |
| for (var i = 0; i < bytes.length; i++) { |
| arraybuffer[i] = bytes.charCodeAt(i); |
| } |
| payload = arraybuffer.buffer; |
| } else if (payloadKind == 'S') { |
| payload = window.atob(message.slice(nextSpaceIdx + 2)); |
| } else { |
| payload = JSON.parse(message.slice(nextSpaceIdx + 1)); |
| } |
| cordova.callbackFromNative(callbackId, success, status, [payload], keepCallback); |
| } else { |
| console.log("processMessage failed: invalid message:" + message); |
| } |
| } catch (e) { |
| console.log("processMessage failed: Message: " + message); |
| console.log("processMessage failed: Error: " + e); |
| console.log("processMessage failed: Stack: " + e.stack); |
| } |
| } |
| |
| // This is called from the NativeToJsMessageQueue.java. |
| androidExec.processMessages = function(messages) { |
| if (messages) { |
| messagesFromNative.push(messages); |
| // Check for the reentrant case, and enqueue the message if that's the case. |
| if (messagesFromNative.length > 1) { |
| return; |
| } |
| while (messagesFromNative.length) { |
| // Don't unshift until the end so that reentrancy can be detected. |
| messages = messagesFromNative[0]; |
| // The Java side can send a * message to indicate that it |
| // still has messages waiting to be retrieved. |
| if (messages == '*') { |
| messagesFromNative.shift(); |
| window.setTimeout(pollOnce, 0); |
| return; |
| } |
| |
| var spaceIdx = messages.indexOf(' '); |
| var msgLen = +messages.slice(0, spaceIdx); |
| var message = messages.substr(spaceIdx + 1, msgLen); |
| messages = messages.slice(spaceIdx + msgLen + 1); |
| processMessage(message); |
| if (messages) { |
| messagesFromNative[0] = messages; |
| } else { |
| messagesFromNative.shift(); |
| } |
| } |
| } |
| }; |
| |
| module.exports = androidExec; |