diff options
| author | Jules Laplace <jules@okfoc.us> | 2015-09-10 14:58:03 -0400 |
|---|---|---|
| committer | Jules Laplace <jules@okfoc.us> | 2015-09-10 14:58:03 -0400 |
| commit | d73a4b1c5a2540077607dcc4001acbae85980ae4 (patch) | |
| tree | c30089f1742f9430bb18679dc6664157a5dc66f4 /StoneIsland/platforms/ios/www/cordova-js-src/exec.js | |
| parent | 124e6c0a8d9577b4a30e0b265f5c23d637c41966 (diff) | |
app skeleton
Diffstat (limited to 'StoneIsland/platforms/ios/www/cordova-js-src/exec.js')
| -rw-r--r-- | StoneIsland/platforms/ios/www/cordova-js-src/exec.js | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/StoneIsland/platforms/ios/www/cordova-js-src/exec.js b/StoneIsland/platforms/ios/www/cordova-js-src/exec.js new file mode 100644 index 00000000..32a3f8b5 --- /dev/null +++ b/StoneIsland/platforms/ios/www/cordova-js-src/exec.js @@ -0,0 +1,323 @@ +/* + * + * 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. + * +*/ + +/** + * Creates a gap bridge iframe used to notify the native code about queued + * commands. + */ +var cordova = require('cordova'), + channel = require('cordova/channel'), + utils = require('cordova/utils'), + base64 = require('cordova/base64'), + // XHR mode does not work on iOS 4.2. + // XHR mode's main advantage is working around a bug in -webkit-scroll, which + // doesn't exist only on iOS 5.x devices. + // IFRAME_NAV is the fastest. + // IFRAME_HASH could be made to enable synchronous bridge calls if we wanted this feature. + jsToNativeModes = { + IFRAME_NAV: 0, // Default. Uses a new iframe for each poke. + // XHR bridge appears to be flaky sometimes: CB-3900, CB-3359, CB-5457, CB-4970, CB-4998, CB-5134 + XHR_NO_PAYLOAD: 1, // About the same speed as IFRAME_NAV. Performance not about the same as IFRAME_NAV, but more variable. + XHR_WITH_PAYLOAD: 2, // Flakey, and not as performant + XHR_OPTIONAL_PAYLOAD: 3, // Flakey, and not as performant + IFRAME_HASH_NO_PAYLOAD: 4, // Not fully baked. A bit faster than IFRAME_NAV, but risks jank since poke happens synchronously. + IFRAME_HASH_WITH_PAYLOAD: 5, // Slower than no payload. Maybe since it has to be URI encoded / decoded. + WK_WEBVIEW_BINDING: 6 // Only way that works for WKWebView :) + }, + bridgeMode, + execIframe, + execHashIframe, + hashToggle = 1, + execXhr, + requestCount = 0, + vcHeaderValue = null, + commandQueue = [], // Contains pending JS->Native messages. + isInContextOfEvalJs = 0, + failSafeTimerId = 0; + +function shouldBundleCommandJson() { + if (bridgeMode === jsToNativeModes.XHR_WITH_PAYLOAD) { + return true; + } + if (bridgeMode === jsToNativeModes.XHR_OPTIONAL_PAYLOAD) { + var payloadLength = 0; + for (var i = 0; i < commandQueue.length; ++i) { + payloadLength += commandQueue[i].length; + } + // The value here was determined using the benchmark within CordovaLibApp on an iPad 3. + return payloadLength < 4500; + } + return false; +} + +function massageArgsJsToNative(args) { + if (!args || utils.typeName(args) != 'Array') { + return args; + } + var ret = []; + args.forEach(function(arg, i) { + if (utils.typeName(arg) == 'ArrayBuffer') { + ret.push({ + 'CDVType': 'ArrayBuffer', + 'data': base64.fromArrayBuffer(arg) + }); + } else { + ret.push(arg); + } + }); + return ret; +} + +function massageMessageNativeToJs(message) { + if (message.CDVType == 'ArrayBuffer') { + var stringToArrayBuffer = function(str) { + var ret = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + ret[i] = str.charCodeAt(i); + } + return ret.buffer; + }; + var base64ToArrayBuffer = function(b64) { + return stringToArrayBuffer(atob(b64)); + }; + message = base64ToArrayBuffer(message.data); + } + return message; +} + +function convertMessageToArgsNativeToJs(message) { + var args = []; + if (!message || !message.hasOwnProperty('CDVType')) { + args.push(message); + } else if (message.CDVType == 'MultiPart') { + message.messages.forEach(function(e) { + args.push(massageMessageNativeToJs(e)); + }); + } else { + args.push(massageMessageNativeToJs(message)); + } + return args; +} + +function iOSExec() { + if (bridgeMode === undefined) { + bridgeMode = jsToNativeModes.IFRAME_NAV; + } + + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.cordova && window.webkit.messageHandlers.cordova.postMessage) { + bridgeMode = jsToNativeModes.WK_WEBVIEW_BINDING; + } + + var successCallback, failCallback, service, action, actionArgs, splitCommand; + var callbackId = null; + if (typeof arguments[0] !== "string") { + // FORMAT ONE + successCallback = arguments[0]; + failCallback = arguments[1]; + service = arguments[2]; + action = arguments[3]; + actionArgs = arguments[4]; + + // Since we need to maintain backwards compatibility, we have to pass + // an invalid callbackId even if no callback was provided since plugins + // will be expecting it. The Cordova.exec() implementation allocates + // an invalid callbackId and passes it even if no callbacks were given. + callbackId = 'INVALID'; + } else { + // FORMAT TWO, REMOVED + try { + splitCommand = arguments[0].split("."); + action = splitCommand.pop(); + service = splitCommand.join("."); + actionArgs = Array.prototype.splice.call(arguments, 1); + + console.log('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' + + "cordova.exec(null, null, \"" + service + "\", \"" + action + "\"," + JSON.stringify(actionArgs) + ");" + ); + return; + } catch (e) {} + } + + // If actionArgs is not provided, default to an empty array + actionArgs = actionArgs || []; + + // Register the callbacks and add the callbackId to the positional + // arguments if given. + if (successCallback || failCallback) { + callbackId = service + cordova.callbackId++; + cordova.callbacks[callbackId] = + {success:successCallback, fail:failCallback}; + } + + actionArgs = massageArgsJsToNative(actionArgs); + + var command = [callbackId, service, action, actionArgs]; + + // Stringify and queue the command. We stringify to command now to + // effectively clone the command arguments in case they are mutated before + // the command is executed. + commandQueue.push(JSON.stringify(command)); + + if (bridgeMode === jsToNativeModes.WK_WEBVIEW_BINDING) { + window.webkit.messageHandlers.cordova.postMessage(command); + } else { + // If we're in the context of a stringByEvaluatingJavaScriptFromString call, + // then the queue will be flushed when it returns; no need for a poke. + // Also, if there is already a command in the queue, then we've already + // poked the native side, so there is no reason to do so again. + if (!isInContextOfEvalJs && commandQueue.length == 1) { + pokeNative(); + } + } +} + +function pokeNative() { + switch (bridgeMode) { + case jsToNativeModes.XHR_NO_PAYLOAD: + case jsToNativeModes.XHR_WITH_PAYLOAD: + case jsToNativeModes.XHR_OPTIONAL_PAYLOAD: + pokeNativeViaXhr(); + break; + default: // iframe-based. + pokeNativeViaIframe(); + } +} + +function pokeNativeViaXhr() { + // This prevents sending an XHR when there is already one being sent. + // This should happen only in rare circumstances (refer to unit tests). + if (execXhr && execXhr.readyState != 4) { + execXhr = null; + } + // Re-using the XHR improves exec() performance by about 10%. + execXhr = execXhr || new XMLHttpRequest(); + // Changing this to a GET will make the XHR reach the URIProtocol on 4.2. + // For some reason it still doesn't work though... + // Add a timestamp to the query param to prevent caching. + execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true); + if (!vcHeaderValue) { + vcHeaderValue = /.*\((.*)\)$/.exec(navigator.userAgent)[1]; + } + execXhr.setRequestHeader('vc', vcHeaderValue); + execXhr.setRequestHeader('rc', ++requestCount); + if (shouldBundleCommandJson()) { + execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages()); + } + execXhr.send(null); +} + +function pokeNativeViaIframe() { + // CB-5488 - Don't attempt to create iframe before document.body is available. + if (!document.body) { + setTimeout(pokeNativeViaIframe); + return; + } + if (bridgeMode === jsToNativeModes.IFRAME_HASH_NO_PAYLOAD || bridgeMode === jsToNativeModes.IFRAME_HASH_WITH_PAYLOAD) { + // TODO: This bridge mode doesn't properly support being removed from the DOM (CB-7735) + if (!execHashIframe) { + execHashIframe = document.createElement('iframe'); + execHashIframe.style.display = 'none'; + document.body.appendChild(execHashIframe); + // Hash changes don't work on about:blank, so switch it to file:///. + execHashIframe.contentWindow.history.replaceState(null, null, 'file:///#'); + } + // The delegate method is called only when the hash changes, so toggle it back and forth. + hashToggle = hashToggle ^ 3; + var hashValue = '%0' + hashToggle; + if (bridgeMode === jsToNativeModes.IFRAME_HASH_WITH_PAYLOAD) { + hashValue += iOSExec.nativeFetchMessages(); + } + execHashIframe.contentWindow.location.hash = hashValue; + } else { + // Check if they've removed it from the DOM, and put it back if so. + if (execIframe && execIframe.contentWindow) { + execIframe.contentWindow.location = 'gap://ready'; + } else { + execIframe = document.createElement('iframe'); + execIframe.style.display = 'none'; + execIframe.src = 'gap://ready'; + document.body.appendChild(execIframe); + } + // Use a timer to protect against iframe being unloaded during the poke (CB-7735). + // This makes the bridge ~ 7% slower, but works around the poke getting lost + // when the iframe is removed from the DOM. + // An onunload listener could be used in the case where the iframe has just been + // created, but since unload events fire only once, it doesn't work in the normal + // case of iframe reuse (where unload will have already fired due to the attempted + // navigation of the page). + failSafeTimerId = setTimeout(function() { + if (commandQueue.length) { + pokeNative(); + } + }, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire). + } +} + +iOSExec.jsToNativeModes = jsToNativeModes; + +iOSExec.setJsToNativeBridgeMode = function(mode) { + // Remove the iFrame since it may be no longer required, and its existence + // can trigger browser bugs. + // https://issues.apache.org/jira/browse/CB-593 + if (execIframe) { + if (execIframe.parentNode) { + execIframe.parentNode.removeChild(execIframe); + } + execIframe = null; + } + bridgeMode = mode; +}; + +iOSExec.nativeFetchMessages = function() { + // Stop listing for window detatch once native side confirms poke. + if (failSafeTimerId) { + clearTimeout(failSafeTimerId); + failSafeTimerId = 0; + } + // Each entry in commandQueue is a JSON string already. + if (!commandQueue.length) { + return ''; + } + var json = '[' + commandQueue.join(',') + ']'; + commandQueue.length = 0; + return json; +}; + +iOSExec.nativeCallback = function(callbackId, status, message, keepCallback) { + return iOSExec.nativeEvalAndFetch(function() { + var success = status === 0 || status === 1; + var args = convertMessageToArgsNativeToJs(message); + cordova.callbackFromNative(callbackId, success, status, args, keepCallback); + }); +}; + +iOSExec.nativeEvalAndFetch = function(func) { + // This shouldn't be nested, but better to be safe. + isInContextOfEvalJs++; + try { + func(); + return iOSExec.nativeFetchMessages(); + } finally { + isInContextOfEvalJs--; + } +}; + +module.exports = iOSExec; |
