UNPKG

8.88 kBJavaScriptView Raw
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/*global require, module, atob, document */
23
24/**
25 * Creates a gap bridge iframe used to notify the native code about queued
26 * commands.
27 */
28var cordova = require('cordova'),
29 utils = require('cordova/utils'),
30 base64 = require('cordova/base64'),
31 execIframe,
32 commandQueue = [], // Contains pending JS->Native messages.
33 isInContextOfEvalJs = 0,
34 failSafeTimerId = 0;
35
36function massageArgsJsToNative(args) {
37 if (!args || utils.typeName(args) != 'Array') {
38 return args;
39 }
40 var ret = [];
41 args.forEach(function(arg, i) {
42 if (utils.typeName(arg) == 'ArrayBuffer') {
43 ret.push({
44 'CDVType': 'ArrayBuffer',
45 'data': base64.fromArrayBuffer(arg)
46 });
47 } else {
48 ret.push(arg);
49 }
50 });
51 return ret;
52}
53
54function massageMessageNativeToJs(message) {
55 if (message.CDVType == 'ArrayBuffer') {
56 var stringToArrayBuffer = function(str) {
57 var ret = new Uint8Array(str.length);
58 for (var i = 0; i < str.length; i++) {
59 ret[i] = str.charCodeAt(i);
60 }
61 return ret.buffer;
62 };
63 var base64ToArrayBuffer = function(b64) {
64 return stringToArrayBuffer(atob(b64));
65 };
66 message = base64ToArrayBuffer(message.data);
67 }
68 return message;
69}
70
71function convertMessageToArgsNativeToJs(message) {
72 var args = [];
73 if (!message || !message.hasOwnProperty('CDVType')) {
74 args.push(message);
75 } else if (message.CDVType == 'MultiPart') {
76 message.messages.forEach(function(e) {
77 args.push(massageMessageNativeToJs(e));
78 });
79 } else {
80 args.push(massageMessageNativeToJs(message));
81 }
82 return args;
83}
84
85function iOSExec() {
86
87 var successCallback, failCallback, service, action, actionArgs;
88 var callbackId = null;
89 if (typeof arguments[0] !== 'string') {
90 // FORMAT ONE
91 successCallback = arguments[0];
92 failCallback = arguments[1];
93 service = arguments[2];
94 action = arguments[3];
95 actionArgs = arguments[4];
96
97 // Since we need to maintain backwards compatibility, we have to pass
98 // an invalid callbackId even if no callback was provided since plugins
99 // will be expecting it. The Cordova.exec() implementation allocates
100 // an invalid callbackId and passes it even if no callbacks were given.
101 callbackId = 'INVALID';
102 } else {
103 throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' +
104 'cordova.exec(null, null, \'Service\', \'action\', [ arg1, arg2 ]);'
105 );
106 }
107
108 // If actionArgs is not provided, default to an empty array
109 actionArgs = actionArgs || [];
110
111 // Register the callbacks and add the callbackId to the positional
112 // arguments if given.
113 if (successCallback || failCallback) {
114 callbackId = service + cordova.callbackId++;
115 cordova.callbacks[callbackId] =
116 {success:successCallback, fail:failCallback};
117 }
118
119 actionArgs = massageArgsJsToNative(actionArgs);
120
121 var command = [callbackId, service, action, actionArgs];
122
123 // Stringify and queue the command. We stringify to command now to
124 // effectively clone the command arguments in case they are mutated before
125 // the command is executed.
126 commandQueue.push(JSON.stringify(command));
127
128 // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
129 // then the queue will be flushed when it returns; no need for a poke.
130 // Also, if there is already a command in the queue, then we've already
131 // poked the native side, so there is no reason to do so again.
132 if (!isInContextOfEvalJs && commandQueue.length == 1) {
133 pokeNative();
134 }
135}
136
137// CB-10530
138function proxyChanged() {
139 var cexec = cordovaExec();
140
141 return (execProxy !== cexec && // proxy objects are different
142 iOSExec !== cexec // proxy object is not the current iOSExec
143 );
144}
145
146// CB-10106
147function handleBridgeChange() {
148 if (proxyChanged()) {
149 var commandString = commandQueue.shift();
150 while(commandString) {
151 var command = JSON.parse(commandString);
152 var callbackId = command[0];
153 var service = command[1];
154 var action = command[2];
155 var actionArgs = command[3];
156 var callbacks = cordova.callbacks[callbackId] || {};
157
158 execProxy(callbacks.success, callbacks.fail, service, action, actionArgs);
159
160 commandString = commandQueue.shift();
161 };
162 return true;
163 }
164
165 return false;
166}
167
168function pokeNative() {
169 // CB-5488 - Don't attempt to create iframe before document.body is available.
170 if (!document.body) {
171 setTimeout(pokeNative);
172 return;
173 }
174
175 // Check if they've removed it from the DOM, and put it back if so.
176 if (execIframe && execIframe.contentWindow) {
177 execIframe.contentWindow.location = 'gap://ready';
178 } else {
179 execIframe = document.createElement('iframe');
180 execIframe.style.display = 'none';
181 execIframe.src = 'gap://ready';
182 document.body.appendChild(execIframe);
183 }
184 // Use a timer to protect against iframe being unloaded during the poke (CB-7735).
185 // This makes the bridge ~ 7% slower, but works around the poke getting lost
186 // when the iframe is removed from the DOM.
187 // An onunload listener could be used in the case where the iframe has just been
188 // created, but since unload events fire only once, it doesn't work in the normal
189 // case of iframe reuse (where unload will have already fired due to the attempted
190 // navigation of the page).
191 failSafeTimerId = setTimeout(function() {
192 if (commandQueue.length) {
193 // CB-10106 - flush the queue on bridge change
194 if (!handleBridgeChange()) {
195 pokeNative();
196 }
197 }
198 }, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire).
199}
200
201iOSExec.nativeFetchMessages = function() {
202 // Stop listing for window detatch once native side confirms poke.
203 if (failSafeTimerId) {
204 clearTimeout(failSafeTimerId);
205 failSafeTimerId = 0;
206 }
207 // Each entry in commandQueue is a JSON string already.
208 if (!commandQueue.length) {
209 return '';
210 }
211 var json = '[' + commandQueue.join(',') + ']';
212 commandQueue.length = 0;
213 return json;
214};
215
216iOSExec.nativeCallback = function(callbackId, status, message, keepCallback, debug) {
217 return iOSExec.nativeEvalAndFetch(function() {
218 var success = status === 0 || status === 1;
219 var args = convertMessageToArgsNativeToJs(message);
220 function nc2() {
221 cordova.callbackFromNative(callbackId, success, status, args, keepCallback);
222 }
223 setTimeout(nc2, 0);
224 });
225};
226
227iOSExec.nativeEvalAndFetch = function(func) {
228 // This shouldn't be nested, but better to be safe.
229 isInContextOfEvalJs++;
230 try {
231 func();
232 return iOSExec.nativeFetchMessages();
233 } finally {
234 isInContextOfEvalJs--;
235 }
236};
237
238// Proxy the exec for bridge changes. See CB-10106
239
240function cordovaExec() {
241 var cexec = require('cordova/exec');
242 var cexec_valid = (typeof cexec.nativeFetchMessages === 'function') && (typeof cexec.nativeEvalAndFetch === 'function') && (typeof cexec.nativeCallback === 'function');
243 return (cexec_valid && execProxy !== cexec)? cexec : iOSExec;
244}
245
246function execProxy() {
247 cordovaExec().apply(null, arguments);
248};
249
250execProxy.nativeFetchMessages = function() {
251 return cordovaExec().nativeFetchMessages.apply(null, arguments);
252};
253
254execProxy.nativeEvalAndFetch = function() {
255 return cordovaExec().nativeEvalAndFetch.apply(null, arguments);
256};
257
258execProxy.nativeCallback = function() {
259 return cordovaExec().nativeCallback.apply(null, arguments);
260};
261
262module.exports = execProxy;