Open driesdriessen opened 4 years ago
Thanks, this would be an enhancement that may need additional testing and support efforts if added. Unfortunately the Cordova maintainers are already overloaded, see apache/cordova#163.
I have an issue in my app that is similar to this one and I think is described here:
https://stackoverflow.com/questions/29515700/phonegap-handleopenurl
Basically what happens is that I associated my app with some file types (GPX, KML) and the app opens the files if the app is already open.
This doesn't work well when the app is not open in the background.
Since I'm using cordova in my production app I have no problems testing this to make sure this works.
I also don't mind sending a PR to solve this, except the other PR I sent was not merged (and was not closed too), so I'm not sure how to properly help...
I'm not sure the solution suggested above is the right solution as there's already a solution using handleOpenUrl
method which I think is the right one, there's only a need to call this function/raise the relevant event in the didFinishLaunchingWithOptions
method, I think.
In any case, I'd love to help move this enhancement forward, just direct me to the right direction/approach...
I stand corrected.
TL;RD: there's a need to add setTimeout(..., 0)
to the following line:
https://github.com/apache/cordova-ios/blob/7e3402c565c2e34eae2bb954c65c989f71e20df1/CordovaLib/Classes/Private/Plugins/CDVHandleOpenURL/CDVHandleOpenURL.m#L62
Explanation
The handleOpenUrl
is called from the objective-c code when the app starts, the problem is that this code can run before the "main" deviceready
event of the rest of the app, and cause some kind of race condition.
I think that in order to solve this the above suggestion is required.
Anther way to solve this using a hack is to add a script to the index.html
to handle the case where the method is called before it is initialized (for example if the device ready method is async and can switch away from execution before the registration) and store the value inside a global member and later check that member to see if we "missed" the invocation. i.e:
<!-- this code should be before the cordova.js script include-->
<script>
window.handleOpenURL = (url) => {
if (typeof(window.handleExternalUrl) === "function") {
window.handleExternalUrl(url);
} else {
window.externalUrl = url;
}
}
</script>
And after that instead of defining window.handleOpenUrl
check if externalUrl
is defined and use and delete it and register for the new method defined above:
if (window.externalUrl) {
doSomething(window.externalUrl);
delete window.externalUrl;
}
window.handleExternalUrl = (url: string) => doSomething(url);
Hope this will help someone else in the future.
Please let me know if you want a PR for the addition of the setTimeout
that is required above...
So if I understand correctly, the race condition is deviceready
may fire and as a result, may attempt to call handleOpenURL
, before the application has a chance to set handleOpenURL
.
I don't think setTimeout
is the proper solution here.
NSString stringWithFormat:@"document.addEventListener('deviceready',function(){if (typeof h
the javascript being invoked from native adds a listener to deviceready
. This means it will either wait until the deviceready
event is fired, or if it's already fired then deviceready
will be fired immediately. Adding a setTimeout
to that logic will force execution to be delayed until the next JS callstack, but I don't think that will guarantee to solve the race condition.
The document parses the script tags synchronously by default. As long as you don't have the async
or defer
attributes set and you're not using Javascript Modules (which are async by default). So for example if you had the following html:
<script src="cordova.js"></script>
<script src="myapp.js"></script>
This should work as expected, as long as myapp.js
sets handleOpenURL
synchronously. The webview will parse and execute cordova.js
then myapp.js
. It should never fire deviceready
before myapp.js
is finish executing because it parses scripts synchronously. However, myapp.js
should ensure it sets handleOpenURL
in a synchronously way. Ie don't wait to set it in any callbacks.
If you want to have reassurance, you could reverse the order and execute myapp.js
before cordova.js
, like you suggested but you'd have to keep in mind that the cordova API hasn't been loaded yet at all, thus you can't use document.addEventListener('deviceready', ...);
Thanks for getting back to me so quickly! :-)
As far as I understand, and I might be wrong here using document.addEventListener('deviceready, ...)
is not deterministic and we want to make sure that when someone writes a handler for the handleOpenURL
inside the deviceready
event this call is not "missed".
As far as I know, the only way to ensure it is by adding a setTimeout(..., 0)
to make sure that the handleOpenURL
that is called in the objective-c code is not executed before the deviceready
that the cordova developer wirtes.
Am I missing something here?
I tested my assumption yesterday by playing with the code I'm writing and with the code in the objective-c code, and adding setTimeout
was the solution that seems to be the "right" one when looking at frameworks like ionic and angular as the js code that is being executed in the client side.
As far as I know, the only way to ensure it is by adding a setTimeout(..., 0) to make sure that the handleOpenURL that is called in the objective-c code is not executed before the deviceready that the cordova developer wirtes.
The objective-c code uses deviceready
:
document.addEventListener('deviceready', function() {
if (typeof handleOpenURL === 'function') {
handleOpenURL(\"%@\");
}
});
Where %@
is a native string placeholder to be replaced with another value.
So handleOpenURL
will never be called before deviceready
because it's inside the deviceready
event. But you do still need to make sure handleOpenURL
is defined before this occurs.
So using my both example where you may have something like this in your html:
<script src="cordova.js"></script>
<script src="myapp.js"></script>
And if you have myapp.js
: :heavy_check_mark:
var handleOpenURL = function(url) {
// do stuff
};
document.addEventListener('deviceready', function() {
// initialize app
});
I'm like 99% sure that would work just fine, as long as you declare the handleOpenURL
where it is assigned as myapp.js
file is parsed by the browser.
If you do the following: :x:
document.addEventListener('deviceready', function() {
var handleOpenURL = function(url) {
// This will definitely cause a race condition
};
// initialize app
});
setTimeout(function() {
var handleOpenURL = function(url) {
// This will also cause a race condition, because handleOpenURL is being set asynchronously.
};
}, 0);
I agree with what you wrote. Consider the following scenario:
document.addEventListener('deviceready', function() {
window.handleOpenURL = function(url) {
// I want to be inside deviceready to make sure I have everything needed to run the code inside this callback...
};
});
As a cordova developer, everything written about cordova says - do this and that after deviceready
so I, as a Cordova developer, am used to the concept of placing everything inside this function and thus the problem...
If you can think of a better way than setTimeout
feel free to suggest. :-)
A possible solution for this would be to change the code in the objective-c to be (store the url in case someone "missed" the opportunity to register the handleOpenURL
callback):
document.addEventListener('deviceready', function() {
if (typeof handleOpenURL === 'function') {
handleOpenURL(\"%@\");
} else {
_handleOpenURL = \"%@\"
}
});
This will allow this kind of scenario to be handled inside the deviceready
function...
I have a similar code for the android written in the same manor, see here (register for an intent and also check the current intent to see how the app started):
https://github.com/IsraelHikingMap/Site/blob/90330c431add8adbbce7b84cde7cac7a14b17b11/IsraelHiking.Web/sources/application/services/open-with.service.ts#L79L89
In any case, I have added the relevant code to workaround this temporarily, but I feel other may benefit from the insight I got...
The relevant commit to solve this is here:
https://github.com/IsraelHikingMap/Site/commit/1932f23b1cd71f6a021005e2975260e3a2e4d47b#diff-c69e535eea05a4913a7ff20510cbffd1da88417599f190e072bf9a503fdd69ec
Also there's a question here if this shouldn't be the same for both Android and iOS to allow people like me to write the code once for both platforms, but this is a different question I guess...
Putting aside for a moment the reliability issues with the JavaScript handleOpenURL
implementation, the original issue here appears to be that CDVAppDelegate's barebones application:didFinishLaunchingWithOptions:
does not do anything with the URL used to launch the app. application:openURL:options:
is not called in that case, so no CDVPluginHandleOpenURLNotification is ever dispatched.
This issue seems fixable (albeit with a risk of double notifications in the case where plugins have registered their own handling based on UIApplicationDidFinishLaunchingNotification notifications, but there is no way to detect that).
Feature Request
Motivation Behind Feature
It is possible to launch an app with start-up parameters. This is useful for opening an app in a specific context. An example is opening an editor with a specific file. Unfortunately startup parameters currently are not passed to the Cordova app.
Feature Description
An app can be launched with parameters: myapp?var1=foo
In the Cordova app, the parameters can be picked up: var url = new URL(globalThis.location;); var var1 = url.searchParams.get("var1");
Alternatives or Workarounds
For IOS I currently copy this code in xCode manually to didFinishLaunchingWithOptions in CDVAppDelegate.m
It would be nice if this can become a part of the standard IOS implementation, and if a similar solution can be created for other platforms.