Sharing Files on Android or iOS from your Qt App

It‘s a common usecase to share Files or Content from native Android or iOS Apps with other Apps on your phone. So I thought this would be an easy task to add sharing to my mobile Apps built with QtQuickControls2.

Found the Blog from Eskil Abrahamsen Blomfeld about Intents with Qt for Android, part 1. Please read that Blog to learn about Android Intents, Qt Android Extras, JNI and HowTo use it from Qt. All of this was new to me – never did JNI before. Also I‘m not an experienced native developer for Android or iOS – that‘s the reason why I‘m using Qt for mobile App development.

I also found Share on iOS and Android using QML, where I learned HowTo share Text and a URL and HowTo structure a QtCreator project for Android and iOS with native code integration.

Unfortunately all of this didn‘t help me to share Files on Android or iOS from Qt mobile Apps.

For a DropBox-like project at customer site I needed sharing:

  • download Files from cloud
  • store Files in App sandbox data
  • view or edit Files in other Apps
  • print Files to local Printer

It took me some time and thanks to much help from other developers at Qt Forum and Slack (QtMob) I found ways to solve my use-cases.

The hardest part was the ugly JNI and Obj-C syntax ;-)

Hopefully my experiences will make it easier for others to integrate Sharing-functionality into their Android or iOS Apps.

Here‘s my Example App at Github.

The Example App demonstrates:

  • Share Text and URL
  • View File
  • Edit File
  • Send File as Stream to ...

To easy understand the complex structure please take a look at this Overview:

share_overview

UI: SwipeView with some Tabs. Each Tab contains a Page with some Buttons to test features.

01_tab_bar

 

 

 

Share Text and URL is the easiest one:

02_android_share_text_url

 

 

 

 

 

 

ShareUtils is registered as „shareUtils“:

void ApplicationUI::addContextProperty(QQmlContext *context)
{
    context->setContextProperty("shareUtils", mShareUtils);
}

Click on the Button to share Text and URL:

Button {
    text: qsTr("Share Text and Url")
    onClicked: {
        shareUtils.share("Qt","http://qt.io")
    }
}

Here‘s the share() method:

Q_INVOKABLE void share(const QString &text, const QUrl &url);
void ShareUtils::share(const QString &text, const QUrl &url)
{
    mPlatformShareUtils->share(text, url);
}

ShareUtils delegates this to PlatformShareUtils:

  • AndroidShareUtils on Android
  • IosShareUtils on iOS

The magic to detect the right class can be found in ShareUtils:

#if defined(Q_OS_IOS)
    mPlatformShareUtils = new IosShareUtils(this);
#elif defined(Q_OS_ANDROID)
    mPlatformShareUtils = new AndroidShareUtils(this);
#else
    mPlatformShareUtils = new PlatformShareUtils(this);
#endif

The Compiler knows the Platform you‘re running on and instantiates the matching platform specificClass.

Android:

void AndroidShareUtils::share(const QString &text, const QUrl &url)
{
    QAndroidJniObject jsText = QAndroidJniObject::fromString(text);
    QAndroidJniObject jsUrl = QAndroidJniObject::fromString(url.toString());
    jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
                                              "share",
                                              "(Ljava/lang/String;Ljava/lang/String;)Z",
                                              jsText.object<jstring>(), jsUrl.object<jstring>());
    if(!ok) {
        emit shareNoAppAvailable(0);
    }
}

QAndroidJniObjects are created for text and url and a Method from QShareUtils.java will be called:

public static boolean share(String text, String url) {
        if (QtNative.activity() == null)
            return false;
        Intent sendIntent = new Intent();
        sendIntent.setAction(Intent.ACTION_SEND);
        sendIntent.putExtra(Intent.EXTRA_TEXT, text + " " + url);
        sendIntent.setType("text/plain");
        // Verify that the intent will resolve to an activity
        if (sendIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) {
            QtNative.activity().startActivity(sendIntent);
            return true;
        } else {
            Log.d("ekkescorner share", "Intent not resolved");
        }
        return false;
    }

In our QShareUtils.java we create the Intent with ACTION_SEND and EXTRA_TEXT and start the Activity using QtNative.activity().

It‘s important to test if the Intent can be resolved – otherwise your App will crash.

Here‘s the iOS implementation:

void IosShareUtils::share(const QString &text, const QUrl &url) {
    NSMutableArray *sharingItems = [NSMutableArray new];
    if (!text.isEmpty()) {
        [sharingItems addObject:text.toNSString()];
    }
    if (url.isValid()) {
        [sharingItems addObject:url.toNSURL()];
    }
    // get the main window rootViewController
    UIViewController *qtUIViewController = [[UIApplication sharedApplication].keyWindow rootViewController];
    UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
    if ( [activityController respondsToSelector:@selector(popoverPresentationController)] ) { // iOS8
        activityController.popoverPresentationController.sourceView = qtUIViewController.view;
    }
    [qtUIViewController presentViewController:activityController animated:YES completion:nil];
}

In this case we‘re using UIActivityViewController to get the share options displayed.

Let‘s take a look HowTo view or edit Files. My Example App contains an Image and a PDF File in assets:

03_assets

At first start of the Example App we copy these files from Assets to (QstandardPaths::AppDataLocation)/my_share_files. This simulates that you have downloaded the Files from your Cloud or WebService.

See all the details in the sources.

Now we want to view or edit these Files in another App. I learned that it‘s not possible to share Files from your AppData Location (Sandbox) – you must copy the Files from APP Data to USER Data – per ex. Documents Location. I create a app specific working directory at DocumentsLocation.

Attention: I‘m not checking if Permissions are set – this isn‘t the goal of this App. To access Files at DocumentLocation you have to set WRITE_EXTERNAL_STORAGE.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Using Files from DocumentsLocation sounds easy but makes things more complicated. We don‘t want to leave those Files at DocumentsLocation when sharing the File finished. Then we must delete them. And if the User edited the File we must know if the File was changed and saved or canceled. If the File was modified we must replace the origin File inside our AppData before deleting the File from DocumentsLocation.

Also after sending the File as a Stream per ex. to a Printer we must delete the copied File as soon as printing was done.

The workflows are different for Android and iOS.

Android needs

  • FilePath URI
  • a specific Action: ACTION_VIEW, ACTION_EDIT, ACTION_SEND
  • MimeType

to find matching Apps.

iOS needs

  • FilePath as NSURL

On iOS there‘s no way to limit the Apps for View or Edit capabilities.

Android does an in-place-Editing of the File, where iOS copies the File over to the other App. To get the modified File back a second manual step is required by the user.

file_flow

The interesting question now is „HowTo know when sharing is finished and if Edit mode was canceled or saved“ ?

Android gives us the startActivityForResult() to get a Result Code back.

Unfortunately I didn‘t found a way to get this Result Code back from the Java Code in QShareUtils.java.

Using Java code on the other side is much easier to read and write as JNI code.

There‘s a workaround: as soon as the Intent Activity is started, our Qt App changes ApplicationState to Suspended and when the Intent Activity closes, the ApplicationState goes back to Active. You can watch the ApplicationState to know when sharing was finished and you also can compare modified Timestamp of the File to recognize changes from Edit mode.

This workflow is marked ‚A‘ in the Overview at the beginning of this article.

More coding, but much more flexible is to use JNI to construct the complete Intent Activity and to go with QAndroidActivityResultReceiver To get the Result back.

Here‘s the method to construct the Edit Intent:

    QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://"+filePath);
    if(!jniPath.isValid()) {
        emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid"));
        return;
    }
    // next step: convert filePath Java String into Java Uri
    QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object<jstring>());
    if(!jniUri.isValid()) {
        emit shareError(requestId, tr("Share: an Error occured\nURI not valid"));
        return;
    }
    // THE INTENT ACTION
    // create a Java String for the ACTION
    QAndroidJniObject jniParam = QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "ACTION_EDIT");
    if(!jniParam.isValid()) {
        emit shareError(requestId, tr("Share: an Error occured"));
        return;
    }
    // then create the Intent Object for this Action
    QAndroidJniObject jniIntent("android/content/Intent","(Ljava/lang/String;)V",jniParam.object<jstring>());
    if(!jniIntent.isValid()) {
        emit shareError(requestId, tr("Share: an Error occured"));
        return;
    }
    // THE FILE TYPE
    if(mimeType.isEmpty()) {
        emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty"));
        return;
    }
    // create a Java String for the File Type (Mime Type)
    QAndroidJniObject jniType = QAndroidJniObject::fromString(mimeType);
    if(!jniType.isValid()) {
        emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid"));
        return;
    }
    // set Data (the URI) and Type (MimeType)
    QAndroidJniObject jniResult = jniIntent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;", jniUri.object<jobject>(), jniType.object<jstring>());
    if(!jniResult.isValid()) {
        emit shareError(requestId, tr("Share: an Error occured"));
        return;
    }
    QAndroidJniObject activity = QtAndroid::androidActivity();
    QAndroidJniObject packageManager = activity.callObjectMethod("getPackageManager",
                                                                 "()Landroid/content/pm/PackageManager;");
    QAndroidJniObject componentName = jniIntent.callObjectMethod("resolveActivity",
                                                              "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;",
                                                              packageManager.object());
    if (!componentName.isValid()) {
        emit shareNoAppAvailable(requestId);
        return;
    }
    // now all is ready to start the Activity:
    // we have the JNI Object, know the requestId
    // and want the Result back into 'this' handleActivityResult(...)
    QtAndroid::startActivity(jniIntent, requestId, this);

Thanks to QAndroidActivityResultReceiver we‘ll get the Result back into:

void AndroidShareUtils::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data)
{
    Q_UNUSED(data);
    // we're getting RESULT_OK only if edit is done
    if(resultCode == RESULT_OK) {
        emit shareEditDone(receiverRequestCode);
    } else if(resultCode == RESULT_CANCELED) {
        emit shareFinished(receiverRequestCode);
    } else {
        emit shareError(receiverRequestCode, tr("Share: an Error occured"));
    }
}

Now it‘s easy to connect to the SIGNAL (shareEditDone, shareFinished, shareError) from QML.This workflow is marked as ‚B‘ in the Overview above.

Please note: the JNI code is different for VIEW/EDIT and SEND – please take a look at all the details in the sources.

Was not so easy to figure out all the details but now it‘s really easy for you: just copy the code into your app :)

On iOS we‘re using a UIDocumentInteractionController:

    NSString* nsFilePath = filePath.toNSString();
    NSURL *nsFileUrl = [NSURL fileURLWithPath:nsFilePath];
    static DocViewController* docViewController = nil;
    if(docViewController!=nil)
    {
        [docViewController removeFromParentViewController];
        [docViewController release];
    }
    UIDocumentInteractionController* documentInteractionController = nil;
    documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:nsFileUrl];
    UIViewController* qtUIViewController = [[[[UIApplication sharedApplication]windows] firstObject]rootViewController];
    if(qtUIViewController!=nil)
    {
        docViewController = [[DocViewController alloc] init];
        docViewController.requestId = requestId;
        // we need this to be able to execute handleDocumentPreviewDone() method,
        // when preview was finished
        docViewController.mIosShareUtils = this;
        [qtUIViewController addChildViewController:docViewController];
        documentInteractionController.delegate = docViewController;
        [documentInteractionController presentPreviewAnimated:YES];
    }

Know when Preview is done from DocViewController:

-
(void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller
{ #pragma unused (controller) self.mIosShareUtils->handleDocumentPreviewDone(self.requestId); [self removeFromParentViewController]; }

We detect the end of preview and call documentPreviewDone() in IosShareUtils:

void IosShareUtils::handleDocumentPreviewDone(const int &requestId)
{
    emit shareFinished(requestId);
}

Now from QML we can connect to the SIGNAL and know when sharing was done.

The good thing: the workflow now is similar for Android and iOS and easier to handle from common QML code.

Please take a look at .pro – there are some Android and iOS specific sections. Also dont forget to add AndroidExtras.

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

Some Screenshots

Open in... Dialog on Android. Select the App you want to use for View or Edit mode:

04_android_share_chooser

Edit the Image and cancel or save:

05_android_share_edit_image

ACTION_SEND enables you to print Text, Images or PDF Files.

Choose the Printer Plugin:

06_android_share_send_chooser

Then select your Printer, configure the Printer and print:

07_android_share_print

On iOS the UIDocumentInteractionController gives you a preview of the File and provides the iOS Share Button at bottom left:

08_ios_preview

The Share Button presents the available Apps:

09_ios_share

Conclusion: you can share Files on Android and iOS and integrate into your specific workflows. SIGNALS are mitted to let you know what the User has done.

There‘s even a call to check if for a given MimeType and Action Type any matching Apps are available on Android.

Call this check before downloading a File from Cloud, to provide a better UX: instead of downloading and then telling users that there‘s no App available you can inform in advance.

One question is open for now: HowTo suppress the Preview on iOS and only show what you get from clicking on the Share Button. Thanks to any tips from Qt / Obj-C experts.

I‘ll continue work on this app and add some more Intent Actions for Android.Also will add functionality to open the App from outside by providing a File URI or MimeType.

Now have fun!


Blog Topics:

Comments