2015-02-07 08:40:15 +08:00
|
|
|
/*
|
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
2019-10-22 00:26:17 +08:00
|
|
|
var cordova = require('cordova');
|
|
|
|
var nativeApiProvider = require('cordova/android/nativeapiprovider');
|
|
|
|
var utils = require('cordova/utils');
|
|
|
|
var base64 = require('cordova/base64');
|
|
|
|
var channel = require('cordova/channel');
|
|
|
|
var jsToNativeModes = {
|
|
|
|
PROMPT: 0,
|
|
|
|
JS_OBJECT: 1
|
|
|
|
};
|
|
|
|
var 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
|
|
|
|
};
|
|
|
|
var jsToNativeBridgeMode; // Set lazily.
|
|
|
|
var nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE;
|
|
|
|
var pollEnabled = false;
|
|
|
|
var bridgeSecret = -1;
|
2015-02-07 08:40:15 +08:00
|
|
|
|
|
|
|
var messagesFromNative = [];
|
|
|
|
var isProcessing = false;
|
2019-10-22 00:26:17 +08:00
|
|
|
var resolvedPromise = typeof Promise === 'undefined' ? null : Promise.resolve();
|
|
|
|
var nextTick = resolvedPromise ? function (fn) { resolvedPromise.then(fn); } : function (fn) { setTimeout(fn); };
|
2015-02-07 08:40:15 +08:00
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function androidExec (success, fail, service, action, args) {
|
2015-02-07 08:40:15 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2016-09-21 17:06:01 +08:00
|
|
|
// If args is not provided, default to an empty array
|
|
|
|
args = args || [];
|
|
|
|
|
2015-02-07 08:40:15 +08:00
|
|
|
// Process any ArrayBuffers in the args into a string.
|
|
|
|
for (var i = 0; i < args.length; i++) {
|
2019-10-22 00:26:17 +08:00
|
|
|
if (utils.typeName(args[i]) === 'ArrayBuffer') {
|
2015-02-07 08:40:15 +08:00
|
|
|
args[i] = base64.fromArrayBuffer(args[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
var callbackId = service + cordova.callbackId++;
|
|
|
|
var argsJson = JSON.stringify(args);
|
2015-02-07 08:40:15 +08:00
|
|
|
if (success || fail) {
|
2019-10-22 00:26:17 +08:00
|
|
|
cordova.callbacks[callbackId] = { success: success, fail: fail };
|
2015-02-07 08:40:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2019-10-22 00:26:17 +08:00
|
|
|
if (jsToNativeBridgeMode === jsToNativeModes.JS_OBJECT && msgs === '@Null arguments.') {
|
2015-02-07 08:40:15 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
androidExec.init = function () {
|
2015-02-07 08:40:15 +08:00
|
|
|
bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode);
|
|
|
|
channel.onNativeReady.fire();
|
|
|
|
};
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function pollOnceFromOnlineEvent () {
|
2015-02-07 08:40:15 +08:00
|
|
|
pollOnce(true);
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function pollOnce (opt_fromOnlineEvent) {
|
2015-02-07 08:40:15 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function pollingTimerFunc () {
|
2015-02-07 08:40:15 +08:00
|
|
|
if (pollEnabled) {
|
|
|
|
pollOnce();
|
|
|
|
setTimeout(pollingTimerFunc, 50);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function hookOnlineApis () {
|
|
|
|
function proxyEvent (e) {
|
2015-02-07 08:40:15 +08:00
|
|
|
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;
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
androidExec.setJsToNativeBridgeMode = function (mode) {
|
|
|
|
if (mode === jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
|
2015-02-07 08:40:15 +08:00
|
|
|
mode = jsToNativeModes.PROMPT;
|
|
|
|
}
|
2019-10-22 00:26:17 +08:00
|
|
|
nativeApiProvider.setPreferPrompt(mode === jsToNativeModes.PROMPT);
|
2015-02-07 08:40:15 +08:00
|
|
|
jsToNativeBridgeMode = mode;
|
|
|
|
};
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
androidExec.setNativeToJsBridgeMode = function (mode) {
|
|
|
|
if (mode === nativeToJsBridgeMode) {
|
2015-02-07 08:40:15 +08:00
|
|
|
return;
|
|
|
|
}
|
2019-10-22 00:26:17 +08:00
|
|
|
if (nativeToJsBridgeMode === nativeToJsModes.POLLING) {
|
2015-02-07 08:40:15 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
if (mode === nativeToJsModes.POLLING) {
|
2015-02-07 08:40:15 +08:00
|
|
|
pollEnabled = true;
|
|
|
|
setTimeout(pollingTimerFunc, 1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function buildPayload (payload, message) {
|
2015-02-07 08:40:15 +08:00
|
|
|
var payloadKind = message.charAt(0);
|
2019-10-22 00:26:17 +08:00
|
|
|
if (payloadKind === 's') {
|
2015-02-07 08:40:15 +08:00
|
|
|
payload.push(message.slice(1));
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 't') {
|
2015-02-07 08:40:15 +08:00
|
|
|
payload.push(true);
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 'f') {
|
2015-02-07 08:40:15 +08:00
|
|
|
payload.push(false);
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 'N') {
|
2015-02-07 08:40:15 +08:00
|
|
|
payload.push(null);
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 'n') {
|
2015-02-07 08:40:15 +08:00
|
|
|
payload.push(+message.slice(1));
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 'A') {
|
2015-02-07 08:40:15 +08:00
|
|
|
var data = message.slice(1);
|
|
|
|
payload.push(base64.toArrayBuffer(data));
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 'S') {
|
2015-02-07 08:40:15 +08:00
|
|
|
payload.push(window.atob(message.slice(1)));
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (payloadKind === 'M') {
|
2015-02-07 08:40:15 +08:00
|
|
|
var multipartMessages = message.slice(1);
|
2019-10-22 00:26:17 +08:00
|
|
|
while (multipartMessages !== '') {
|
2015-02-07 08:40:15 +08:00
|
|
|
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.
|
2019-10-22 00:26:17 +08:00
|
|
|
function processMessage (message) {
|
2015-02-07 08:40:15 +08:00
|
|
|
var firstChar = message.charAt(0);
|
2019-10-22 00:26:17 +08:00
|
|
|
if (firstChar === 'J') {
|
2015-02-07 08:40:15 +08:00
|
|
|
// This is deprecated on the .java side. It doesn't work with CSP enabled.
|
2019-10-22 00:26:17 +08:00
|
|
|
// eslint-disable-next-line no-eval
|
2015-02-07 08:40:15 +08:00
|
|
|
eval(message.slice(1));
|
2019-10-22 00:26:17 +08:00
|
|
|
} else if (firstChar === 'S' || firstChar === 'F') {
|
|
|
|
var success = firstChar === 'S';
|
|
|
|
var keepCallback = message.charAt(1) === '1';
|
2015-02-07 08:40:15 +08:00
|
|
|
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 {
|
2019-10-22 00:26:17 +08:00
|
|
|
console.log('processMessage failed: invalid message: ' + JSON.stringify(message));
|
2015-02-07 08:40:15 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function processMessages () {
|
2015-02-07 08:40:15 +08:00
|
|
|
// 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.
|
2019-10-22 00:26:17 +08:00
|
|
|
if (msg === '*' && messagesFromNative.length === 0) {
|
2015-02-07 08:40:15 +08:00
|
|
|
nextTick(pollOnce);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
processMessage(msg);
|
|
|
|
} finally {
|
|
|
|
isProcessing = false;
|
|
|
|
if (messagesFromNative.length > 0) {
|
|
|
|
nextTick(processMessages);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 00:26:17 +08:00
|
|
|
function popMessageFromQueue () {
|
2015-02-07 08:40:15 +08:00
|
|
|
var messageBatch = messagesFromNative.shift();
|
2019-10-22 00:26:17 +08:00
|
|
|
if (messageBatch === '*') {
|
2015-02-07 08:40:15 +08:00
|
|
|
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;
|