How to automatically launch iOS app with parameters
-
Qt 5.12
I'd like to launch our iOS app with some optional parameters unique to the user (an operating mode, a server URL and access key). If the app is launched without these, the user is prompted to enter these on startup. On the desktop, these parameters can be specified as arguments on the command line.
It looks like the way to do this on iOS is through a URL with a custom scheme. I've added CFBundleURLTypes to the Info.plist and I can now launch my app from a browser with a custom URL like:
camcops://default_single_user_mode//default_server_location/https%3A%2F%2Fserver.example.com%2Fapi/default_access_key/fomom-nobij-hirug-hukor-rudal-nukup-kilum-fanif-b
So far so good.
The bit I'm struggling with is how to read the URL parameters and pass them into the application. I can see there is the setUrlHandler() method on the QDesktopServices class documented at https://doc.qt.io/qt-5/qdesktopservices.html and referenced at https://stackoverflow.com/questions/28822086/best-way-to-get-source-url-of-custom-ios-scheme-in-qml but if I set the URL handler once the app is launched it will be too late won't it?
Elsewhere I see people extending QIOSApplicationDelegate but I don't understand how this would glue together with my application.
Has anyone done this before? Am I going about it the right way?
-
I got it working for a URL like camcops://camcops.org/register/?default_single_user_mode=true&default_server_location=https%3A%2F%2Fserver.example.com%2Fapi&default_access_key=abcde-fghij-klmno-pqrst-uvwxy-zabcd-efghi-jklmn-o and the same on Android but with http scheme. That seems to be the preferred approach on each platform. Unfortunately some email clients such as GMail do not display URLs with unknown schemes as hyperlinks, even if they are within <a> elements in HTML.
Full code is at https://github.com/RudolfCardinal/camcops/pull/185 under GPL V3
Here are some highlights:
Info.plist:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Viewer</string> <key>CFBundleURLSchemes</key> <array> <string>camcops</string> </array> </dict> </array>
In the main application:
auto url_handler = UrlHandler::getInstance(); connect(url_handler, &UrlHandler::defaultSingleUserModeSet, this, &CamcopsApp::setDefaultSingleUserMode); connect(url_handler, &UrlHandler::defaultServerLocationSet, this, &CamcopsApp::setDefaultServerLocation); connect(url_handler, &UrlHandler::defaultAccessKeySet, this, &CamcopsApp::setDefaultAccessKey);
urlhandler.cpp:
UrlHandler* UrlHandler::m_instance = NULL; UrlHandler::UrlHandler() { m_instance = this; QDesktopServices::setUrlHandler("camcops", this, "handleUrl"); } void UrlHandler::handleUrl(const QUrl url) { auto query = QUrlQuery(url); auto default_single_user_mode = query.queryItemValue("default_single_user_mode"); if (!default_single_user_mode.isEmpty()) { emit defaultSingleUserModeSet(default_single_user_mode); } auto default_server_location = query.queryItemValue("default_server_location", QUrl::FullyDecoded); if (!default_server_location.isEmpty()) { emit defaultServerLocationSet(default_server_location); } auto default_access_key = query.queryItemValue("default_access_key"); if (!default_access_key.isEmpty()) { emit defaultAccessKeySet(default_access_key); } } UrlHandler* UrlHandler::getInstance() { if (!m_instance) m_instance = new UrlHandler; return m_instance; }
Android doesn't support the same approach so we have to write some Java:
public class CamcopsActivity extends QtActivity { // Defined in urlhandler.cpp public static native void handleAndroidUrl(String url); @Override public void onCreate(Bundle savedInstanceState) { // Called when no instance of the app is running. Pass URL parameters // as arguments to the app's main() Intent intent = getIntent(); if (intent != null && intent.getAction() == Intent.ACTION_VIEW) { Uri uri = intent.getData(); if (uri != null) { Log.i(TAG, intent.getDataString()); Map<String, String> parameters = getQueryParameters(uri); StringBuilder sb = new StringBuilder(); String separator = ""; for (Map.Entry<String, String> entry : parameters.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); if (value != null) { sb.append(separator) .append("--").append(name) .append("=").append(value); separator = "\t"; } } APPLICATION_PARAMETERS = sb.toString(); } } super.onCreate(savedInstanceState); } @Override public void onNewIntent(Intent intent) { /* Called when the app is already running. Send the URL parameters * as signals to the app. */ super.onNewIntent(intent); sendUrlToApp(intent); } private void sendUrlToApp(Intent intent) { String url = intent.getDataString(); if (url != null) { handleAndroidUrl(url); } } private Map<String, String> getQueryParameters(Uri uri) { List<String> names = Arrays.asList("default_single_user_mode", "default_server_location", "default_access_key"); Map<String, String> parameters = new HashMap<String, String>(); for (String name : names) { String value = uri.getQueryParameter(name); if (value != null) { parameters.put(name, value); } } return parameters; }
urlhandler.cpp:
#ifdef Q_OS_ANDROID // Called from android/src/org/camcops/camcops/CamcopsActivity.java #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_org_camcops_camcops_CamcopsActivity_handleAndroidUrl( JNIEnv *env, jobject obj, jstring url) { Q_UNUSED(obj) const char *url_str = env->GetStringUTFChars(url, NULL); UrlHandler::getInstance()->handleUrl(QUrl(url_str)); env->ReleaseStringUTFChars(url, url_str); } #ifdef __cplusplus } #endif #endif
and then in AndroidManifest.xml:
<activity ... android:name="org.camcops.camcops.CamcopsActivity" ... launchMode="singleTask"> ... <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="http" android:host="camcops.org" android:path="/register/"/> </intent-filter> </activity>
-
Hi,
Wouldn't it be simpler to store these in settings and load them from there ?
-
@SGaist Possibly. I wasn't aware of QSettings. Now I am. Thank you.
This defaults are only needed the first time the app is used for registration. Any other user settings we store encrypted in a SQLite database.
-
I may have misunderstood something. You users will receive these information in an email to get started ?
-
I may have misunderstood something. You users will receive these information in an email to get started ?
@SGaist Yes exactly. It's an app for completing medical tasks such as questionnaires. The patients are set up on a server and emailed a URL with a unique access key and a server address. The first time the app is used, it registers with the server and downloads the questionnaires. After that the URL is no longer needed.
-
I got it working for a URL like camcops://camcops.org/register/?default_single_user_mode=true&default_server_location=https%3A%2F%2Fserver.example.com%2Fapi&default_access_key=abcde-fghij-klmno-pqrst-uvwxy-zabcd-efghi-jklmn-o and the same on Android but with http scheme. That seems to be the preferred approach on each platform. Unfortunately some email clients such as GMail do not display URLs with unknown schemes as hyperlinks, even if they are within <a> elements in HTML.
Full code is at https://github.com/RudolfCardinal/camcops/pull/185 under GPL V3
Here are some highlights:
Info.plist:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Viewer</string> <key>CFBundleURLSchemes</key> <array> <string>camcops</string> </array> </dict> </array>
In the main application:
auto url_handler = UrlHandler::getInstance(); connect(url_handler, &UrlHandler::defaultSingleUserModeSet, this, &CamcopsApp::setDefaultSingleUserMode); connect(url_handler, &UrlHandler::defaultServerLocationSet, this, &CamcopsApp::setDefaultServerLocation); connect(url_handler, &UrlHandler::defaultAccessKeySet, this, &CamcopsApp::setDefaultAccessKey);
urlhandler.cpp:
UrlHandler* UrlHandler::m_instance = NULL; UrlHandler::UrlHandler() { m_instance = this; QDesktopServices::setUrlHandler("camcops", this, "handleUrl"); } void UrlHandler::handleUrl(const QUrl url) { auto query = QUrlQuery(url); auto default_single_user_mode = query.queryItemValue("default_single_user_mode"); if (!default_single_user_mode.isEmpty()) { emit defaultSingleUserModeSet(default_single_user_mode); } auto default_server_location = query.queryItemValue("default_server_location", QUrl::FullyDecoded); if (!default_server_location.isEmpty()) { emit defaultServerLocationSet(default_server_location); } auto default_access_key = query.queryItemValue("default_access_key"); if (!default_access_key.isEmpty()) { emit defaultAccessKeySet(default_access_key); } } UrlHandler* UrlHandler::getInstance() { if (!m_instance) m_instance = new UrlHandler; return m_instance; }
Android doesn't support the same approach so we have to write some Java:
public class CamcopsActivity extends QtActivity { // Defined in urlhandler.cpp public static native void handleAndroidUrl(String url); @Override public void onCreate(Bundle savedInstanceState) { // Called when no instance of the app is running. Pass URL parameters // as arguments to the app's main() Intent intent = getIntent(); if (intent != null && intent.getAction() == Intent.ACTION_VIEW) { Uri uri = intent.getData(); if (uri != null) { Log.i(TAG, intent.getDataString()); Map<String, String> parameters = getQueryParameters(uri); StringBuilder sb = new StringBuilder(); String separator = ""; for (Map.Entry<String, String> entry : parameters.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); if (value != null) { sb.append(separator) .append("--").append(name) .append("=").append(value); separator = "\t"; } } APPLICATION_PARAMETERS = sb.toString(); } } super.onCreate(savedInstanceState); } @Override public void onNewIntent(Intent intent) { /* Called when the app is already running. Send the URL parameters * as signals to the app. */ super.onNewIntent(intent); sendUrlToApp(intent); } private void sendUrlToApp(Intent intent) { String url = intent.getDataString(); if (url != null) { handleAndroidUrl(url); } } private Map<String, String> getQueryParameters(Uri uri) { List<String> names = Arrays.asList("default_single_user_mode", "default_server_location", "default_access_key"); Map<String, String> parameters = new HashMap<String, String>(); for (String name : names) { String value = uri.getQueryParameter(name); if (value != null) { parameters.put(name, value); } } return parameters; }
urlhandler.cpp:
#ifdef Q_OS_ANDROID // Called from android/src/org/camcops/camcops/CamcopsActivity.java #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_org_camcops_camcops_CamcopsActivity_handleAndroidUrl( JNIEnv *env, jobject obj, jstring url) { Q_UNUSED(obj) const char *url_str = env->GetStringUTFChars(url, NULL); UrlHandler::getInstance()->handleUrl(QUrl(url_str)); env->ReleaseStringUTFChars(url, url_str); } #ifdef __cplusplus } #endif #endif
and then in AndroidManifest.xml:
<activity ... android:name="org.camcops.camcops.CamcopsActivity" ... launchMode="singleTask"> ... <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="http" android:host="camcops.org" android:path="/register/"/> </intent-filter> </activity>
This post is deleted!