Sharing Files on Android or iOS from or with your Qt App - Part 2

Please read Part 1 of this Blog series about sharing Files on Android and iOS where I wrote about my experiences how to share Files or Content from your Qt App with native Android or iOS Apps.

In the meantime I did some UI tuning and added more info to make it easier to understand and to manage some more use-cases, so expect some more parts of this series next weeks.

Also I found out that not all Android Apps are good citizens of ACTION_EDIT - Intents with ResultCode. After editing and save per ex. Google Fotos sends correct RESULT_OK where other Apps are giving you RESULT_CANCEL. For our workflows it‘s important to know if a File was modified, so as a workaround I store the last-modified-Timestamp before starting the Intent and compare with current Timestamp when Result was handled. All of this is done under the hood and you‘re always getting the correct result code back from EDIT Action.

Some preliminary notes

  • Try it out: all sources are available at GitHub
  • Permissions not checked in Example App: enable WRITE_EXTERNAL_STORAGE manually
  • Current release is for Target SDK 23 ! (Android 7 requires FileProvider – will add later)
  • All Use-Cases are implemented to be used x-platform – currently Android and iOS

If you‘re looking for an Android-only solution please also take a look at AndroidNative project.

Android / iOS

First part covers sharing Files from your App with other Apps on Android and iOS.

This Blog Post is about sharing Files with your Qt App on Android (iOS will follow soon):

  • Use your App as Share Target to receive Content or Files from other Android Apps
  • Impacts on „Sharing Files from your App with other Apps“ (see Part 1)

Intent Filter

To tell the Android OS what kind of Files our App can handle we must add an Intent Filter to AndroidManifest.xml:

<activity … >
        <!-- Handle shared incoming urls -->
        <intent-filter>
            <action android:name="android.intent.action.SEND"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <data android:mimeType="*/*"/>
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <data android:mimeType="*/*"/>
            <data android:scheme="file"/>
            <data android:scheme="content"/>
        </intent-filter>
</activity>

Now the App can receive Intents for all kind of MimeType using ACTION_VIEW or ACTION_SEND.

Custom Android Activity

To be able to get the Intent we must add some code to our main Activity, so extend the default QtActivity. To do so create a QShareActivity.java at this location:

<project>/android/src/org/ekkescorner/examples/sharex

public class QShareActivity extends QtActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }

As next change the Name in AndroidManifest.xml:

<activity ... android:name="org.ekkescorner.examples.sharex.QShareActivity" ...>

Get the incoming Intent

Now the interesting part:
Our App can be opened by other Apps - how do we get the Intent data and read or copy the ‚foreign‘ File ?

This depends from current ApplicationState:
if the App is already running, Android OS will call onNewIntent() - if not our App will be launched and the Intent comes in from onCreate(). The best way is to check the Intent from both methods and then call another method to process the Intent.

Attention: there are some traps to watch !

To inform the UI about an incoming File as usual we want to use a SIGNAL.

If the App is already running we want to open our App exactly on the current Page. Imagine a DropBox-like App where you navigate deep into your File hierarchy and now you want to upload a new Presentation File into the current Folder, but you must edit the Presentation before. So you open PowerPoint or something else, create/edit the presentation and then want to share that File with your App. Of course the User wants to be exactly where he was before without the need to do deep File navigation again.

If our App will be launched by the incoming Intent, onCreate() was called from Android OS to deliver the Intent. Unfortunately at this moment the Qt App isn‘t ready and the SIGNAL will be lost. To avoid this don‘t process the Intent from onCreate() but remember that there‘s a pending Intent.

Later when the Qt App is ready check for pending Intents. HowTo know if the App is ready to process Intents ?

In our Example App we‘re checking the ApplicationState and first time the State becomes Active we ask for pending Intents.

In a real-life business App probably you must at first Login to your Server or get some data from Server, so you‘ll do the check for pending Intents later.

LaunchMode ‚singleInstance‘ vs ‚singleTask‘

As next I found out that the last opened Page wasn‘t shown – instead a white Screen comes up and an error was logged: ‚Surface1 null‘.

Reason was the Launch Mode. Most Android Intent examples are using ‚singleTask‘ so I also did it. Changing the LaunchMode to ‚singleInstance‘ in combination with ‚taskAffinity‘ works better for Qt Apps: now the App opens correct the last opened Page.

Here are the changes to AndroidManifest.xml:

<activity ... android:launchMode="singleInstance" android:taskAffinity="">

Some code snippets:

QShareActivity.java:

public class QShareActivity extends QtActivity
{
    public static boolean isIntentPending;
    public static boolean isInitialized;
    public static String workingDirPath;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // now we're checking if the App was started from another Android App via Intent Intent theIntent = getIntent(); if (theIntent != null){ String theAction = theIntent.getAction(); if (theAction != null){ // delay processIntent(); isIntentPending = true; } } } // if we are opened from other apps: @Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); // Intent will be processed, if all is initialized and Qt / QML can handle the event if(isInitialized) { processIntent(); } else { isIntentPending = true; } } public void checkPendingIntents(String workingDir) { isInitialized = true; workingDirPath = workingDir; if(isIntentPending) { isIntentPending = false; processIntent(); } } // process the Intent if Action is SEND or VIEW private void processIntent(){ Intent intent = getIntent(); // do something with the Intent } }

Check ApplicationState in ApplicationUI.cpp:

#if defined(Q_OS_ANDROID)
void ApplicationUI::onApplicationStateChanged(Qt::ApplicationState applicationState)
{
    if(applicationState == Qt::ApplicationState::ApplicationActive) {
        // if App was launched from VIEW or SEND Intent
        // there's a race collision: the event will be lost,
        // because App and UI wasn't completely initialized
        // workaround: QShareActivity remembers that an Intent is pending
        if(!mPendingIntentsChecked) {
            mPendingIntentsChecked = true;
            mShareUtils->checkPendingIntents(mAppDataFilesPath);
        }
    }
}
#endif

Process the incoming Intent

Now let‘s take a look at processIntent() from QShareActivity.java to see how to read the File from the other Android App.

We‘re listening for VIEW and SEND Intent Actions. The ways to get the Uri are different:

Uri intentUri;
String intentScheme;
String intentAction;
if (intent.getAction().equals("android.intent.action.VIEW")){
       intentAction = "VIEW";
       intentUri = intent.getData();
} else if (intent.getAction().equals("android.intent.action.SEND")){
       intentAction = "SEND";
        Bundle bundle = intent.getExtras();
        intentUri = (Uri)bundle.get(Intent.EXTRA_STREAM);
} else {
        return;
}

As next we must check the Scheme to know if it‘s a ‚file‘ or ‚content‘ Scheme.

// content or file
intentScheme = intentUri.getScheme();
if (intentScheme == null){
      return;
}
if(intentScheme.equals("file")){
      // URI as encoded string
      setFileUrlReceived(intentUri.toString());
      // we are done Qt can deal with file scheme
      return;
}
if(!intentScheme.equals("content")){
        return;
}

To get the real FilePath from a Content Uri isn‘t so easy. Found many complicated examples and finally I got a great solution to extract the absolute FilePath from the Content Uri :)

Please take a look at QSharePathResolver.java:

<project>/android/src/org/ekkescorner/utils/

public class QSharePathResolver { public static String getRealPathFromURI(final Context context, final Uri uri) { // ... } }

QSharePathResolver.java checks if the content Uri comes from

  • ExternalStorageProvider
  • DownloadsProvider
  • MediaProvider: Images, Video, Audio
  • GoogleFotosUri

and tries to calculate the real path. Now it‘s easy to get the File from an Intent providing a Content Uri:

filePath = QSharePathResolver.getRealPathFromURI(this, intentUri);
setFileUrlReceived(filePath);
// we are done Qt can deal with file scheme
return;

If the Uri couldn‘t be resolved from QSharePathResolver.java we try to read the InputStream. Please take a look at QShareUtils.java: createFile(ContentResolver cR, Uri uri, String fileLocation)

filePath = QShareUtils.createFile(cR, intentUri, workingDirPath);
setFileReceivedAndSaved(filePath);

01_new_intent_from_other_app

Ok – we received the File from our custom Activity – how can we provide this to our QML UI where we‘re waiting for a SIGNAL from C++ ?

From Java (Activity) to C++ to emit the SIGNAL for QML

The methods setFileUrlReceived() and setFileReceivedAndSaved() are native methods:

public class QShareActivity extends QtActivity
{
    // 'file' scheme or resolved from 'content' scheme:
    public static native void setFileUrlReceived(String url);
    // InputStream from 'content' scheme:
    public static native void setFileReceivedAndSaved(String url);
}

Native Methods are implemented in C++ via JNICALL – see all details in AndroidShareUtils.cpp:

#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL
  Java_org_ekkescorner_examples_sharex_QShareActivity_setFileUrlReceived(JNIEnv *env,
                                        jobject obj,
                                        jstring url)
{
    const char *urlStr = env->GetStringUTFChars(url, NULL);
    Q_UNUSED (obj)
    AndroidShareUtils::getInstance()->setFileUrlReceived(urlStr);
    env->ReleaseStringUTFChars(url, urlStr);
    return;
}
JNIEXPORT void JNICALL
  Java_org_ekkescorner_examples_sharex_QShareActivity_setFileReceivedAndSaved(JNIEnv *env,
                                        jobject obj,
                                        jstring url)
{
    const char *urlStr = env->GetStringUTFChars(url, NULL);
    Q_UNUSED (obj)
    AndroidShareUtils::getInstance()->setFileReceivedAndSaved(urlStr);
    env->ReleaseStringUTFChars(url, urlStr);
    return;
}
#ifdef __cplusplus
}
#endif

If you want to learn more about JNICALL and other ways to manage them, here‘s a great Blog Post from BogDan Vatra : qt-android-episode-5.

JNICALL is like a bridge from Java to C++:

void AndroidShareUtils::setFileUrlReceived(const QString &url)
{
    QString myUrl;
    if(url.startsWith("file://")) {
        myUrl= url.right(url.length()-7);
    } else {
        myUrl= url;
    }
    // check if File exists
    QFileInfo fileInfo = QFileInfo(myUrl);
    if(fileInfo.exists()) {
        emit fileUrlReceived(myUrl);
    } else {
        emit shareError(0, tr("File does not exist: %1").arg(myUrl));
    }
}

This JNICALL enables us to emit the SIGNAL that we received an Intent from another Android App providing access to a File.

02_process_intent_from_other_app

Test it !

Now it‘s a good point to test it. Open Google Photos, select an Image and ShareWith shows ‚ekkes SHARE Example‘ App as target:

03_see_our_app_as_target

Select ‚ekkes SHARE Example‘ and Android will open our App. If the App already was opened, the current Page will be displayed and include the Image:

04_our_app_received_image_from_other_app

onActivityResult() (JAVA) vs QAndroidActivityResultReceiver (JNI)

While writing Part 1 of this Blog series I didn‘t found a way to get the Result back if Intent was started with Result from QShareUtils.java (Workflow ‚A‘ - the JAVA way)

QtNative.activity().startActivityForResult(Intent.createChooser(viewIntent, title), requestId);

@hamalaiv comment pointed me to the right direction: to get the Result back a Custom Activity must be used.

Ok – we‘re now using a custom Activity (QShareActivity.java) – let‘s test it.

To get the Result back, we must be implemented this JAVA code:

public class QShareActivity extends QtActivity
{
    public static native void fireActivityResult(int requestCode, int resultCode);

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { fireActivityResult(requestCode, resultCode); } }

and this C / C++ Code:

JNIEXPORT void JNICALL
  Java_org_ekkescorner_examples_sharex_QShareActivity_fireActivityResult(JNIEnv *env,
                                        jobject obj,
                                        jint requestCode,
                                        jint resultCode)
{
    Q_UNUSED (obj)
    Q_UNUSED (env)
    AndroidShareUtils::getInstance()->onActivityResult(requestCode, resultCode);
    return;
}
void AndroidShareUtils::onActivityResult(int requestCode, int resultCode)
{
    processActivityResult(requestCode, resultCode);
}
void AndroidShareUtils::processActivityResult(int requestCode, int resultCode)
{
    if(resultCode == RESULT_OK) {
        emit shareEditDone(requestCode);
    } else if(resultCode == RESULT_CANCELED) {
		if(mIsEditMode) {
			// check Timestamp and
			emit shareEditDone(requestCode);
			// or
			emit shareFinished(requestCode);
			return;
        }
        emit shareFinished(requestCode);
    } else {
        emit shareError(requestCode, tr("Share: an Error occured"));
    }
}

05_overview_share_files_with_other_apps_v2

Testing the JAVA way sharing Files with other Android Apps now works as expected with or without ResultCode. I removed the workaround code from ApplicationUI.

But there‘s a Drawback: Testing the JNI way (Part 1, Workflow ‚B‘) using QAndroidActivityResultReceiver only works without ResultCode. If starting the Intent with ResultCode

QtAndroid::startActivity(jniIntent, requestId, this);

the QAndroidActivityResultReceiver wasn‘t called for the Result: instead the Result comes back via our custom Activity onActivityResult() method using a wrong RequestId.

Lesson learned: never use QAndroidActivityResultReceiver (C++, JNI) and onActivityResult() (JAVA Activity) together in one App.

To test this please comment or rename onActivityResult() and you‘ll see the JNI implementation alone works well.

There‘s another problem if we want to share one of our Files with other Apps, because now we‘re source and also target for same MimeTypes. This means: our own App will be listed as a target:

06_own_app_is_target

Tried some ways to workaround, but doesn‘t work well and selecting the own App as Target with Result crashed the App because there was a collision with LaunchMode ‚singleInstance‘.

Unfortunately there‘s no way to exclude targets using Intent Filter. So we have to create a custom Chooser without listing our own App as Target. Thanks to this thread at StackOverFlow I found a way to solve this. Please take a look at QShareUtils.java

public static boolean createCustomChooserAndStartActivity(Intent theIntent, String title, int requestId) {
	// ...
}

Here‘s a short summary:

  • Create Intent as before and use as template
  • Context is QtNative.activity()
  • Get packageManager from context.getPackageManager()
  • Retrieve all Apps (List) for Template - Intent: packageManager.queryIntentActivities()
  • Sort Apps (List) by Label
  • From Apps (List) create List
    • Create Intent as Copy from Template - Intent
    • Add setPackage(targetPackageName)
    • Don‘t add Intent to list if Target PackageName equals own PackageName
    • If needed also watch a Blacklist

We created a List of Intents, where all Intents are based on our previously created Template - Intent with an extra PackageName added. This means each Intent will only detect one specific App as Target.

Now the tricky part:

We want to all collected Intents as EXTRA_INITIAL_INTENTS and we‘re using the last one from list of Intents as initializing Intent because the Chooser adds its initializing Intent to the end of EXTRA_INITIAL_INTENTS :)

Intent chooserIntent = Intent.createChooser(targetedIntents.remove(targetedIntents.size() - 1), title);
if (targetedIntents.isEmpty()) {
    Log.d("ekkescorner", title+" only one Intent left for Chooser");
} else {
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toArray(new Parcelable[] {}));
}

QtNative.activity().startActivityForResult(chooserIntent, requestId);

07_own_app_excluded_from_targets

Now it works as expected: our own App doesn‘t appear in list of target Apps.

Attention: onActivityResult() (JAVA) sometimes is too fast compared with QAndroidActivityResultReceiver (JNI)

While testing Action SEND I noticed that Files are not copied to other Apps or not printed, because ‚File does not exist‘. This happens only if going the JAVA way getting the Result back from onActivityResult(), but not if using JNI and QAndroidActivityResultReceiver.
When getting the Result back I‘m deleting the File from Documents Folder (see Part1) and it seems that onActivityResult() sends the Result before File was fully copied / printed.
As workaround I added a Timer with 500ms delay in QML before deleting the File.

From now on I‘ll use the JAVA way (Workflow ‚A‘ in Overview) as default to share Files with other Apps, because it‘s much easier to implement specific behaviour (as our Chooser Intent) using JAVA instead of JNI. The pure JNI way still remains in the code to get a feeling HowTo solve it without JAVA code.

Share from other iOS Apps with our Qt App

As next I want to implement the same functionality for iOS – so stay tuned for Part 3.

My goal is always to provide easy-to-use x-platform solutions for Android and iOS. That‘s why I‘m using Qt ;-)

Some years ago I developed mobile business Apps for BlackBerry10 (BB10) where the InvocationFramework makes it easy to share content or files between Apps.

BB10 Customers are now (and next years) in transition from BB10 to Android, iOS or both and of course I want to motivate them to use Qt as their new Framework. Last two years I implemented most of my patterns using Qt and QtQuickControls2 for Android and iOS. Having features only for one platform is like having no solution. Step-by-step I‘m reaching feature-parity with BB10.

I‘m also blogging about all of this to make it easy for mobile devs to start with Qt.

Back to Sharing Example.

Need Help – offer Bonus

As I understand it right, iOS Extensions should be used to share Files or Content from other iOS Apps to our Qt App and it seems that this isn‘t supported yet our of the box from QtCreator. If there are some manual steps needed I need a description HowTo do this.

Remember: I‘m a mobile Business App Developer, not a Xcode expert and not an Obj-C expert and I‘m doing all my work from QtCreator – not commandline.

I‘m open for all tips and any help HowTo ...

  • Open Qt App from other iOS Apps sharing Text Content or Files
  • Reopen Qt App from other iOS Apps at current Page if Qt App is already running
  • Emit SIGNAL about received Files or Text and copy Files into AppData location
  • Development workflow to solve this from QtCreator / Xcode

Bonus: If someone can provide a small sample project before I found out all the steps and blogged about, the first one will get a Bonus of 500 € from me. Please be aware that I‘ll modify and include code into my Share Example as Open Source code.

Have Fun

Now it‘s time to download current Version from Github, build and run the Sharing Example App.


Blog Topics:

Comments