Search code examples
androidc++qtjava-native-interfaceapk

Qt/C++/Android - How to install an .APK file programmatically?


I am implementing my own auto-updater within my application. I was able to successfully download the .apk file of the newer version into the /Download folder on the sdcard, but I can't figure out how to open/run that file so the user is presented with the new installation dialog.

The only thing I could come up with:

QString downloadedAPK = "/storage/emulated/0/Download/latest.apk"; // Not hardcoded, but wrote it here this way for simplicity
QDesktopServices::openUrl(QUrl(downloadedAPK));

Debugger output:

D/Instrumentation(26418): checkStartActivityResult  :Intent { act=android.intent.action.VIEW dat=file:///storage/emulated/0/Download/latest.apk }
D/Instrumentation(26418): checkStartActivityResult  inent is instance of inent:
W/System.err(26418): android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=file:///storage/emulated/0/Download/latest.apk }
W/System.err(26418):    at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:1660)
W/System.err(26418):    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1430)
W/System.err(26418):    at android.app.Activity.startActivityForResult(Activity.java:3532)
W/System.err(26418):    at android.app.Activity.startActivityForResult(Activity.java:3493)
W/System.err(26418):    at android.app.Activity.startActivity(Activity.java:3735)
W/System.err(26418):    at android.app.Activity.startActivity(Activity.java:3703)
W/System.err(26418):    at org.qtproject.qt5.android.QtNative.openURL(QtNative.java:110)
W/System.err(26418):    at dalvik.system.NativeStart.run(Native Method)

I have looked everywhere but never found anything regarding opening APKs from Qt. The only thing I found was a solutoin using JNI ( which I don't want to use because it's simpler to just do it with C++ and because I have zero experience with the whole C++/JNI thing ) and it was not well documented so I didn't understand how to make it work.

So, what would be the easiest way to open the downloaded apk?



Edit:

I have followed Tumbus's answer, but because of some compiling errors I had to make a few modifications on his JNI code as follows:

void Updater::InstallApp(const QString &appPackageName)
{
    qDebug() << "[+] APP: " << appPackageName; // Which is the string ("/storage/emulated/0/Download/latest.apk")
    QAndroidJniObject app = QAndroidJniObject::fromString(appPackageName);
    QAndroidJniObject::callStaticMethod<jint>("AndroidIntentLauncher",
                                       "installApp",
                                       "(Ljava/lang/String;)I",
                                       app.object<jstring>());
}

When I run my application on my android device, it pulls the newest .apk file from my server, then nothing happens. Why? (I have not made any changes on the AndroidManifest.xml until now).


Solution

  • You have to make a custom intent to install APK. See this question for details.

    I'm afraid such platform-specific think must require calls to platform-specific API. The good news are Qt framework has simplified wrap-up on JNI and you can include a Java class into Android project. Therefore I would make my own static java function called from Qt.

    Example

    Java class

    package io.novikov.androidintentlauncher;
    
    import org.qtproject.qt5.android.QtNative;
    
    import java.lang.String;
    import java.io.File;
    import android.content.Intent;
    import android.util.Log;
    import android.net.Uri;
    import android.content.ContentValues;
    import android.content.Context;
    
    public class AndroidIntentLauncher
    {
        protected AndroidIntentLauncher()
        {
        }
    
        public static int installApp(String appPackageName) {
            if (QtNative.activity() == null)
                return -1;
            try {
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setDataAndType(Uri.fromFile(new File(appPackageName)), 
                                                   "application/vnd.android.package-archive");
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                QtNative.activity().startActivity(intent);
                return 0;
            } catch (android.content.ActivityNotFoundException anfe) {
                return -3;
            }
        }
    
    }
    

    Notice that startActivity() should be called as a method from *QtNative.activity(). We have to maintain special directory structures for java according to conventional rules. The example is at Makefile section below.

    JNI

    The C++ code to call this method is a bit tricky.

    const static char* MY_JAVA_CLASS = "io/novikov/androidintentlauncher/AndroidIntentLauncher";
    
    
    static void InstallApp(const QString &appPackageName) {
            QAndroidJniObject jsText = QAndroidJniObject::fromString(appPackageName);
            QAndroidJniObject::callStaticMethod<jint>(MY_JAVA_CLASS,
                                               "installApp",
                                               "(Ljava/lang/String;)I",
                                               jsText.object<jstring>());
       }
    

    The string literal "(Ljava/lang/String;)I" is the signature of java method. The name of the Java class must be at a complete form "my/domain/my_app/MyClass"

    Makefile

    The last challenge is to include the java code to your project properly. Below the corresponding fragment of the .pro file.

    android {
        QT += androidextras
        OTHER_FILES += android_src/src/io/novikov/androidintentlauncher/AndroidIntentLauncher.java
        ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android_src
    }
    

    QMake has a special variable ANDROID_PACKAGE_SOURCE_DIR for this job. Java sources must reside in ANDROID_PACKAGE_SOURCE_DIR/src/my/domain directories. Also don't forget to add java source to OTHER_FILES and include androidextras QT option.