Merge v0.6.0 and v0.6.1 #29

Merged
dorian merged 30 commits from version/0.5.1 into master 2024-10-17 17:52:37 +02:00
24 changed files with 1657 additions and 26 deletions
Showing only changes of commit 28400f98e4 - Show all commits

View file

@ -10,7 +10,7 @@
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/> <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon"> <application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop"> <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="de.itsblue.blueROCK.MainActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@ -68,8 +68,24 @@
--> -->
<meta-data android:name="android.app.extract_android_style" android:value="default"/> <meta-data android:name="android.app.extract_android_style" android:value="default"/>
<!-- extract android style --> <!-- extract android style -->
<!-- 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> </activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices --> <!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
<provider android:name="android.support.v4.content.FileProvider" android:authorities="de.itsblue.blueROCK.fileprovider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
</provider>
</application> </application>
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>

View file

@ -18,6 +18,7 @@ apply plugin: 'com.android.application'
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
compile 'com.android.support:support-v4:25.3.1'
} }
android { android {
@ -77,5 +78,6 @@ android {
lintOptions { lintOptions {
checkReleaseBuilds false checkReleaseBuilds false
abortOnError false
} }
} }

View file

@ -0,0 +1,3 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_shared_files" path="share_example_x_files/" />
</paths>

View file

@ -0,0 +1,228 @@
// (c) 2017 Ekkehard Gentz (ekke)
// this project is based on ideas from
// http://blog.lasconic.com/share-on-ios-and-android-using-qml/
// see github project https://github.com/lasconic/ShareUtils-QML
// also inspired by:
// https://www.androidcode.ninja/android-share-intent-example/
// https://www.calligra.org/blogs/sharing-with-qt-on-android/
// https://stackoverflow.com/questions/7156932/open-file-in-another-app
// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData
// OpenURL in At Android: got ideas from:
// https://github.com/BernhardWenzel/open-url-in-qt-android
// https://github.com/tobiatesan/android_intents_qt
//
// see also /COPYRIGHT and /LICENSE
package de.itsblue.blueROCK;
import org.qtproject.qt5.android.QtNative;
import org.qtproject.qt5.android.bindings.QtActivity;
import android.os.*;
import android.content.*;
import android.app.*;
import java.lang.String;
import android.content.Intent;
import java.io.File;
import android.net.Uri;
import android.util.Log;
import android.content.ContentResolver;
import android.webkit.MimeTypeMap;
import org.ekkescorner.utils.*;
public class MainActivity extends QtActivity
{
// native - must be implemented in Cpp via JNI
// '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);
//
public static native void fireActivityResult(int requestCode, int resultCode);
//
public static native boolean checkFileExits(String url);
public static boolean isIntentPending;
public static boolean isInitialized;
public static String workingDirPath;
// Use a custom Chooser without providing own App as share target !
// see QShareUtils.java createCustomChooserAndStartActivity()
// Selecting your own App as target could cause AndroidOS to call
// onCreate() instead of onNewIntent()
// and then you are in trouble because we're using 'singleInstance' as LaunchMode
// more details: my blog at Qt
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("ekkescorner", "onCreate QShareActivity");
// 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){
Log.d("ekkescorner onCreate ", theAction);
// QML UI not ready yet
// delay processIntent();
isIntentPending = true;
}
}
} // onCreate
// WIP - trying to find a solution to survive a 2nd onCreate
// ongoing discussion in QtMob (Slack)
// from other Apps not respecting that you only have a singleInstance
// there are problems per ex. sharing a file from Google Files App,
// but working well using Xiaomi FileManager App
@Override
public void onDestroy() {
Log.d("ekkescorner", "onDestroy QShareActivity");
// super.onDestroy();
// System.exit() closes the App before doing onCreate() again
// then the App was restarted, but looses context
// This works for Samsung My Files
// but Google Files doesn't call onDestroy()
System.exit(0);
}
// we start Activity with result code
// to test JNI with QAndroidActivityResultReceiver you must comment or rename
// this method here - otherwise you'll get wrong request or result codes
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Check which request we're responding to
Log.d("ekkescorner onActivityResult", "requestCode: "+requestCode);
if (resultCode == RESULT_OK) {
Log.d("ekkescorner onActivityResult - resultCode: ", "SUCCESS");
} else {
Log.d("ekkescorner onActivityResult - resultCode: ", "CANCEL");
}
// hint: result comes back too fast for Action SEND
// if you want to delete/move the File add a Timer w 500ms delay
// see Example App main.qml - delayDeleteTimer
// if you want to revoke permissions for older OS
// it makes sense also do this after the delay
fireActivityResult(requestCode, resultCode);
}
// if we are opened from other apps:
@Override
public void onNewIntent(Intent intent) {
Log.d("ekkescorner", "onNewIntent");
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;
}
} // onNewIntent
public void checkPendingIntents(String workingDir) {
isInitialized = true;
workingDirPath = workingDir;
Log.d("ekkescorner", workingDirPath);
if(isIntentPending) {
isIntentPending = false;
Log.d("ekkescorner", "checkPendingIntents: true");
processIntent();
} else {
Log.d("ekkescorner", "nothingPending");
}
} // checkPendingIntents
// process the Intent if Action is SEND or VIEW
private void processIntent(){
Intent intent = getIntent();
Uri intentUri;
String intentScheme;
String intentAction;
// we are listening to android.intent.action.SEND or VIEW (see Manifest)
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 {
Log.d("ekkescorner Intent unknown action:", intent.getAction());
return;
}
Log.d("ekkescorner action:", intentAction);
if (intentUri == null){
Log.d("ekkescorner Intent URI:", "is null");
return;
}
Log.d("ekkescorner Intent URI:", intentUri.toString());
// content or file
intentScheme = intentUri.getScheme();
if (intentScheme == null){
Log.d("ekkescorner Intent URI Scheme:", "is null");
return;
}
if(intentScheme.equals("file")){
// URI as encoded string
Log.d("ekkescorner Intent File URI: ", intentUri.toString());
setFileUrlReceived(intentUri.toString());
// we are done Qt can deal with file scheme
return;
}
if(!intentScheme.equals("content")){
Log.d("ekkescorner Intent URI unknown scheme: ", intentScheme);
return;
}
// ok - it's a content scheme URI
// we will try to resolve the Path to a File URI
// if this won't work or if the File cannot be opened,
// we'll try to copy the file into our App working dir via InputStream
// hopefully in most cases PathResolver will give a path
// you need the file extension, MimeType or Name from ContentResolver ?
// here's HowTo get it:
Log.d("ekkescorner Intent Content URI: ", intentUri.toString());
ContentResolver cR = this.getContentResolver();
MimeTypeMap mime = MimeTypeMap.getSingleton();
String fileExtension = mime.getExtensionFromMimeType(cR.getType(intentUri));
Log.d("ekkescorner","Intent extension: "+fileExtension);
String mimeType = cR.getType(intentUri);
Log.d("ekkescorner"," Intent MimeType: "+mimeType);
String name = QShareUtils.getContentName(cR, intentUri);
if(name != null) {
Log.d("ekkescorner Intent Name:", name);
} else {
Log.d("ekkescorner Intent Name:", "is NULL");
}
String filePath;
filePath = QSharePathResolver.getRealPathFromURI(this, intentUri);
if(filePath == null) {
Log.d("ekkescorner QSharePathResolver:", "filePath is NULL");
} else {
Log.d("ekkescorner QSharePathResolver:", filePath);
// to be safe check if this File Url really can be opened by Qt
// there were problems with MS office apps on Android 7
if (checkFileExits(filePath)) {
setFileUrlReceived(filePath);
// we are done Qt can deal with file scheme
return;
}
}
// trying the InputStream way:
filePath = QShareUtils.createFile(cR, intentUri, workingDirPath);
if(filePath == null) {
Log.d("ekkescorner Intent FilePath:", "is NULL");
return;
}
setFileReceivedAndSaved(filePath);
} // processIntent
} // class QShareActivity

View file

@ -0,0 +1,223 @@
// from: https://github.com/wkh237/react-native-fetch-blob/blob/master/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java
// MIT License, see: https://github.com/wkh237/react-native-fetch-blob/blob/master/LICENSE
// original copyright: Copyright (c) 2017 xeiyan@gmail.com
// src slightly modified to be used into Qt Projects: (c) 2017 ekke@ekkes-corner.org
package org.ekkescorner.utils;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.content.ContentUris;
import android.os.Environment;
import android.content.ContentResolver;
import java.io.File;
import java.io.InputStream;
import java.io.FileOutputStream;
import android.util.Log;
import java.lang.NumberFormatException;
public class QSharePathResolver {
public static String getRealPathFromURI(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
Log.d("ekkescorner"," isExternalStorageDocument");
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
Log.d("ekkescorner"," isDownloadsDocument");
final String id = DocumentsContract.getDocumentId(uri);
Log.d("ekkescorner"," getDocumentId "+id);
long longId = 0;
try
{
longId = Long.valueOf(id);
}
catch(NumberFormatException nfe)
{
return getDataColumn(context, uri, null, null);
}
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), longId);
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
Log.d("ekkescorner"," isMediaDocument");
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
else if ("content".equalsIgnoreCase(uri.getScheme())) {
Log.d("ekkescorner"," is uri.getScheme()");
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
}
// Other Providers
else{
Log.d("ekkescorner ","is Other Provider");
try {
InputStream attachment = context.getContentResolver().openInputStream(uri);
if (attachment != null) {
String filename = getContentName(context.getContentResolver(), uri);
if (filename != null) {
File file = new File(context.getCacheDir(), filename);
FileOutputStream tmp = new FileOutputStream(file);
byte[] buffer = new byte[1024];
while (attachment.read(buffer) > 0) {
tmp.write(buffer);
}
tmp.close();
attachment.close();
return file.getAbsolutePath();
}
}
} catch (Exception e) {
// TODO SIGNAL shareError()
return null;
}
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
Log.d("ekkescorner ","NOT DocumentsContract.isDocumentUri");
Log.d("ekkescorner"," is uri.getScheme()");
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
Log.d("ekkescorner"," return: getDataColumn ");
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
Log.d("ekkescorner ","NOT DocumentsContract.isDocumentUri");
Log.d("ekkescorner"," is file scheme");
return uri.getPath();
}
return null;
}
private static String getContentName(ContentResolver resolver, Uri uri) {
Cursor cursor = resolver.query(uri, null, null, null, null);
cursor.moveToFirst();
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
if (nameIndex >= 0) {
String name = cursor.getString(nameIndex);
cursor.close();
return name;
}
cursor.close();
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
String result = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
result = cursor.getString(index);
}
}
catch (Exception ex) {
ex.printStackTrace();
return null;
}
finally {
if (cursor != null)
cursor.close();
}
return result;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
}

View file

@ -0,0 +1,396 @@
// (c) 2017 Ekkehard Gentz (ekke)
// this project is based on ideas from
// http://blog.lasconic.com/share-on-ios-and-android-using-qml/
// see github project https://github.com/lasconic/ShareUtils-QML
// also inspired by:
// https://www.androidcode.ninja/android-share-intent-example/
// https://www.calligra.org/blogs/sharing-with-qt-on-android/
// https://stackoverflow.com/questions/7156932/open-file-in-another-app
// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData
// https://stackoverflow.com/questions/5734678/custom-filtering-of-intent-chooser-based-on-installed-android-package-name
// see also /COPYRIGHT and /LICENSE
package org.ekkescorner.utils;
import org.qtproject.qt5.android.QtNative;
import java.lang.String;
import android.content.Intent;
import java.io.File;
import android.net.Uri;
import android.util.Log;
import android.content.ContentResolver;
import android.database.Cursor;
import android.provider.MediaStore;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.util.List;
import android.content.pm.ResolveInfo;
import java.util.ArrayList;
import android.content.pm.PackageManager;
import java.util.Comparator;
import java.util.Collections;
import android.content.Context;
import android.os.Parcelable;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.support.v4.app.ShareCompat;
public class QShareUtils
{
// reference Authority as defined in AndroidManifest.xml
private static String AUTHORITY="de.itsblue.blueROCK.fileprovider";
protected QShareUtils()
{
Log.d("ekkescorner", "QShareUtils()");
}
public static boolean checkMimeTypeView(String mimeType) {
if (QtNative.activity() == null)
return false;
Intent myIntent = new Intent();
myIntent.setAction(Intent.ACTION_VIEW);
// without an URI resolve always fails
// an empty URI allows to resolve the Activity
File fileToShare = new File("");
Uri uri = Uri.fromFile(fileToShare);
myIntent.setDataAndType(uri, mimeType);
// Verify that the intent will resolve to an activity
if (myIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) {
Log.d("ekkescorner checkMime ", "YEP - we can go on and View");
return true;
} else {
Log.d("ekkescorner checkMime", "sorry - no App available to View");
}
return false;
}
public static boolean checkMimeTypeEdit(String mimeType) {
if (QtNative.activity() == null)
return false;
Intent myIntent = new Intent();
myIntent.setAction(Intent.ACTION_EDIT);
// without an URI resolve always fails
// an empty URI allows to resolve the Activity
File fileToShare = new File("");
Uri uri = Uri.fromFile(fileToShare);
myIntent.setDataAndType(uri, mimeType);
// Verify that the intent will resolve to an activity
if (myIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) {
Log.d("ekkescorner checkMime ", "YEP - we can go on and Edit");
return true;
} else {
Log.d("ekkescorner checkMime", "sorry - no App available to Edit");
}
return false;
}
public static boolean shareText(String text) {
if (QtNative.activity() == null)
return false;
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, text);
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;
}
// thx @oxied and @pooks for the idea: https://stackoverflow.com/a/18835895/135559
// theIntent is already configured with all needed properties and flags
// so we only have to add the packageName of targeted app
public static boolean createCustomChooserAndStartActivity(Intent theIntent, String title, int requestId, Uri uri) {
final Context context = QtNative.activity();
final PackageManager packageManager = context.getPackageManager();
final boolean isLowerOrEqualsKitKat = Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT;
// MATCH_DEFAULT_ONLY: Resolution and querying flag. if set, only filters that support the CATEGORY_DEFAULT will be considered for matching.
// Check if there is a default app for this type of content.
ResolveInfo defaultAppInfo = packageManager.resolveActivity(theIntent, PackageManager.MATCH_DEFAULT_ONLY);
if(defaultAppInfo == null) {
Log.d("ekkescorner", title+" PackageManager cannot resolve Activity");
return false;
}
// had to remove this check - there can be more Activity names, per ex
// com.google.android.apps.docs.editors.kix.quickword.QuickWordDocumentOpenerActivityAlias
// if (!defaultAppInfo.activityInfo.name.endsWith("ResolverActivity") && !defaultAppInfo.activityInfo.name.endsWith("EditActivity")) {
// Log.d("ekkescorner", title+" defaultAppInfo not Resolver or EditActivity: "+defaultAppInfo.activityInfo.name);
// return false;
//}
// Retrieve all apps for our intent. Check if there are any apps returned
List<ResolveInfo> appInfoList = packageManager.queryIntentActivities(theIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (appInfoList.isEmpty()) {
Log.d("ekkescorner", title+" appInfoList.isEmpty");
return false;
}
Log.d("ekkescorner", title+" appInfoList: "+appInfoList.size());
// Sort in alphabetical order
Collections.sort(appInfoList, new Comparator<ResolveInfo>() {
@Override
public int compare(ResolveInfo first, ResolveInfo second) {
String firstName = first.loadLabel(packageManager).toString();
String secondName = second.loadLabel(packageManager).toString();
return firstName.compareToIgnoreCase(secondName);
}
});
List<Intent> targetedIntents = new ArrayList<Intent>();
// Filter itself and create intent with the rest of the apps.
for (ResolveInfo appInfo : appInfoList) {
// get the target PackageName
String targetPackageName = appInfo.activityInfo.packageName;
// we don't want to share with our own app
// in fact sharing with own app with resultCode will crash because doesn't work well with launch mode 'singleInstance'
if (targetPackageName.equals(context.getPackageName())) {
continue;
}
// if you have a blacklist of apps please exclude them here
// we create the targeted Intent based on our already configured Intent
Intent targetedIntent = new Intent(theIntent);
// now add the target packageName so this Intent will only find the one specific App
targetedIntent.setPackage(targetPackageName);
// collect all these targetedIntents
targetedIntents.add(targetedIntent);
// legacy support and Workaround for Android bug
// grantUriPermission needed for KITKAT or older
// see https://code.google.com/p/android/issues/detail?id=76683
// also: https://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps
if(isLowerOrEqualsKitKat) {
Log.d("ekkescorner", "legacy support grantUriPermission");
context.grantUriPermission(targetPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// attention: you must revoke the permission later, so this only makes sense with getting back a result to know that Intent was done
// I always move or delete the file, so I don't revoke permission
}
}
// check if there are apps found for our Intent to avoid that there was only our own removed app before
if (targetedIntents.isEmpty()) {
Log.d("ekkescorner", title+" targetedIntents.isEmpty");
return false;
}
// now we can create our Intent with custom Chooser
// we need all collected targetedIntents as EXTRA_INITIAL_INTENTS
// we're using the last targetedIntent as initializing Intent, because
// 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[] {}));
}
// Verify that the intent will resolve to an activity
if (chooserIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) {
if(requestId > 0) {
QtNative.activity().startActivityForResult(chooserIntent, requestId);
} else {
QtNative.activity().startActivity(chooserIntent);
}
return true;
}
Log.d("ekkescorner", title+" Chooser Intent not resolved. Should never happen");
return false;
}
// I am deleting the files from shared folder when Activity was done or canceled
// so probably I don't have to revike FilePermissions for older OS
// if you don't delete or move the file: here's what you must done to revoke the access
public static void revokeFilePermissions(String filePath) {
final Context context = QtNative.activity();
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
File file = new File(filePath);
Uri uri = FileProvider.getUriForFile(context, AUTHORITY, file);
context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
public static boolean sendFile(String filePath, String title, String mimeType, int requestId) {
if (QtNative.activity() == null)
return false;
// using v4 support library create the Intent from ShareCompat
// Intent sendIntent = new Intent();
Intent sendIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent();
sendIntent.setAction(Intent.ACTION_SEND);
File imageFileToShare = new File(filePath);
// Using FileProvider you must get the URI from FileProvider using your AUTHORITY
// Uri uri = Uri.fromFile(imageFileToShare);
Uri uri;
try {
uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare);
} catch (IllegalArgumentException e) {
Log.d("ekkescorner sendFile - cannot be shared: ", filePath);
return false;
}
Log.d("ekkescorner sendFile", uri.toString());
sendIntent.putExtra(Intent.EXTRA_STREAM, uri);
if(mimeType == null || mimeType.isEmpty()) {
// fallback if mimeType not set
mimeType = QtNative.activity().getContentResolver().getType(uri);
Log.d("ekkescorner sendFile guessed mimeType:", mimeType);
} else {
Log.d("ekkescorner sendFile w mimeType:", mimeType);
}
sendIntent.setType(mimeType);
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
sendIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
return createCustomChooserAndStartActivity(sendIntent, title, requestId, uri);
}
public static boolean viewFile(String filePath, String title, String mimeType, int requestId) {
if (QtNative.activity() == null)
return false;
// using v4 support library create the Intent from ShareCompat
// Intent viewIntent = new Intent();
Intent viewIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent();
viewIntent.setAction(Intent.ACTION_VIEW);
File imageFileToShare = new File(filePath);
// Using FileProvider you must get the URI from FileProvider using your AUTHORITY
// Uri uri = Uri.fromFile(imageFileToShare);
Uri uri;
try {
uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare);
} catch (IllegalArgumentException e) {
Log.d("ekkescorner viewFile - cannot be shared: ", filePath);
return false;
}
// now we got a content URI per ex
// content://org.ekkescorner.examples.sharex.fileprovider/my_shared_files/qt-logo.png
// from a fileUrl:
// /data/user/0/org.ekkescorner.examples.sharex/files/share_example_x_files/qt-logo.png
Log.d("ekkescorner viewFile from file path: ", filePath);
Log.d("ekkescorner viewFile to content URI: ", uri.toString());
if(mimeType == null || mimeType.isEmpty()) {
// fallback if mimeType not set
mimeType = QtNative.activity().getContentResolver().getType(uri);
Log.d("ekkescorner viewFile guessed mimeType:", mimeType);
} else {
Log.d("ekkescorner viewFile w mimeType:", mimeType);
}
viewIntent.setDataAndType(uri, mimeType);
viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
viewIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
return createCustomChooserAndStartActivity(viewIntent, title, requestId, uri);
}
public static boolean editFile(String filePath, String title, String mimeType, int requestId) {
if (QtNative.activity() == null)
return false;
// using v4 support library create the Intent from ShareCompat
// Intent editIntent = new Intent();
Intent editIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent();
editIntent.setAction(Intent.ACTION_EDIT);
File imageFileToShare = new File(filePath);
// Using FileProvider you must get the URI from FileProvider using your AUTHORITY
// Uri uri = Uri.fromFile(imageFileToShare);
Uri uri;
try {
uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare);
} catch (IllegalArgumentException e) {
Log.d("ekkescorner editFile - cannot be shared: ", filePath);
return false;
}
Log.d("ekkescorner editFile", uri.toString());
if(mimeType == null || mimeType.isEmpty()) {
// fallback if mimeType not set
mimeType = QtNative.activity().getContentResolver().getType(uri);
Log.d("ekkescorner editFile guessed mimeType:", mimeType);
} else {
Log.d("ekkescorner editFile w mimeType:", mimeType);
}
editIntent.setDataAndType(uri, mimeType);
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
return createCustomChooserAndStartActivity(editIntent, title, requestId, uri);
}
public static String getContentName(ContentResolver cR, Uri uri) {
Cursor cursor = cR.query(uri, null, null, null, null);
cursor.moveToFirst();
int nameIndex = cursor
.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
if (nameIndex >= 0) {
return cursor.getString(nameIndex);
} else {
return null;
}
}
public static String createFile(ContentResolver cR, Uri uri, String fileLocation) {
String filePath = null;
try {
InputStream iStream = cR.openInputStream(uri);
if (iStream != null) {
String name = getContentName(cR, uri);
if (name != null) {
filePath = fileLocation + "/" + name;
Log.d("ekkescorner - create File", filePath);
File f = new File(filePath);
FileOutputStream tmp = new FileOutputStream(f);
Log.d("ekkescorner - create File", "new FileOutputStream");
byte[] buffer = new byte[1024];
while (iStream.read(buffer) > 0) {
tmp.write(buffer);
}
tmp.close();
iStream.close();
return filePath;
} // name
} // iStream
} catch (FileNotFoundException e) {
e.printStackTrace();
return filePath;
} catch (IOException e) {
e.printStackTrace();
return filePath;
} catch (Exception e) {
e.printStackTrace();
return filePath;
}
return filePath;
}
}

View file

@ -1,4 +1,4 @@
QT += quick qml quickcontrols2 purchasing widgets QT += quick qml quickcontrols2 purchasing printsupport
CONFIG += c++11 CONFIG += c++11
VERSION = 0.5.0 VERSION = 0.5.0
@ -15,11 +15,24 @@ DEFINES += QT_DEPRECATED_WARNINGS
# You can also select to disable deprecated APIs only up to a certain version of Qt. # You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
INCLUDEPATH *= $$PWD/headers
# Add version to define
DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\"
SOURCES += \ SOURCES += \
sources/shareUtils/platformshareutils.cpp \
sources/appsettings.cpp \ sources/appsettings.cpp \
sources/bluerockbackend.cpp \ sources/bluerockbackend.cpp \
sources/shareUtils/shareutils.cpp \
sources/main.cpp sources/main.cpp
HEADERS += \
headers/appsettings.h \
headers/bluerockbackend.h \
headers/shareUtils/shareutils.h \
headers/shareUtils/platformshareutils.h
RESOURCES += resources/qml/qml.qrc \ RESOURCES += resources/qml/qml.qrc \
resources/shared/shared.qrc \ resources/shared/shared.qrc \
#resources/shared/icons/bluerock/index.theme \ #resources/shared/icons/bluerock/index.theme \
@ -37,15 +50,18 @@ qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target !isEmpty(target.path): INSTALLS += target
# Add version to define
DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\"
HEADERS += \
headers/appsettings.h \
headers/bluerockbackend.h
DISTFILES += \ DISTFILES += \
CHANGELOG.md \ CHANGELOG.md
android {
QT += androidextras
SOURCES += sources/shareUtils/androidshareutils.cpp
HEADERS += headers/shareUtils/androidshareutils.h
OTHER_FILES += android/src/org/ekkescorner/utils/QShareUtils.java \
android/src/org/ekkescorner/utils/QSharePathResolver.java \
android/src/de/itsblue/blueROCK/MainActivity.java \
android/AndroidManifest.xml \ android/AndroidManifest.xml \
android/build.gradle \ android/build.gradle \
android/gradle.properties \ android/gradle.properties \
@ -53,10 +69,8 @@ DISTFILES += \
android/gradle/wrapper/gradle-wrapper.properties \ android/gradle/wrapper/gradle-wrapper.properties \
android/gradlew \ android/gradlew \
android/gradlew.bat \ android/gradlew.bat \
android/res/values/libs.xml android/res/values/libs.xml \
android/res/xml/filepaths.xml
android {
QT += androidextras
defineReplace(droidVersionCode) { defineReplace(droidVersionCode) {
segments = $$split(1, ".") segments = $$split(1, ".")

View file

@ -24,6 +24,17 @@
#include <QEventLoop> #include <QEventLoop>
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
#include <QPainter>
#include <QPrinter>
#include <QPixmap>
#include <QBrush>
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTextCursor>
#include <QPageSize>
#include <QPdfWriter>
#include "shareUtils/shareutils.h"
class BlueRockBackend : public QObject class BlueRockBackend : public QObject
{ {
@ -34,13 +45,16 @@ public:
private: private:
QVariantMap senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery()); QVariantMap senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery());
ShareUtils* _shareUtils;
signals: signals:
public slots: public slots:
QVariant getWidgetData(QVariantMap params); QVariant getWidgetData(QVariantMap params);
QVariantMap getParamsFromUrl(QString url); QVariantMap getParamsFromUrl(QString url);
void shareResultsAsUrl(QString url);
void shareResultsAsPoster(QString url);
}; };

View file

@ -0,0 +1,48 @@
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
// my blog about Qt for mobile: http://j.mp/qt-x
// see also /COPYRIGHT and /LICENSE
#ifndef ANDROIDSHAREUTILS_H
#define ANDROIDSHAREUTILS_H
#include <QtAndroid>
#include <QAndroidActivityResultReceiver>
#include "shareUtils/platformshareutils.h"
class AndroidShareUtils : public PlatformShareUtils, public QAndroidActivityResultReceiver
{
public:
AndroidShareUtils(QObject* parent = nullptr);
bool checkMimeTypeView(const QString &mimeType) override;
bool checkMimeTypeEdit(const QString &mimeType) override;
void shareText(const QString &text) override;
void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override;
void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override;
void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override;
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;
void onActivityResult(int requestCode, int resultCode);
void checkPendingIntents(const QString workingDirPath) override;
static AndroidShareUtils* getInstance();
public slots:
void setFileUrlReceived(const QString &url);
void setFileReceivedAndSaved(const QString &url);
bool checkFileExits(const QString &url);
private:
bool mIsEditMode;
qint64 mLastModified;
QString mCurrentFilePath;
static AndroidShareUtils* mInstance;
void processActivityResult(int requestCode, int resultCode);
};
#endif // ANDROIDSHAREUTILS_H

View file

@ -0,0 +1,46 @@
// (c) 2017 Ekkehard Gentz (ekke)
// this project is based on ideas from
// http://blog.lasconic.com/share-on-ios-and-android-using-qml/
// see github project https://github.com/lasconic/ShareUtils-QML
// also inspired by:
// https://www.androidcode.ninja/android-share-intent-example/
// https://www.calligra.org/blogs/sharing-with-qt-on-android/
// https://stackoverflow.com/questions/7156932/open-file-in-another-app
// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData
// see also /COPYRIGHT and /LICENSE
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
// my blog about Qt for mobile: http://j.mp/qt-x
// see also /COPYRIGHT and /LICENSE
#ifndef PLATFORMSHAREUTILS_H
#define PLATFORMSHAREUTILS_H
#include <QObject>
#include <QDebug>
class PlatformShareUtils : public QObject
{
Q_OBJECT
signals:
void shareEditDone(int requestCode);
void shareFinished(int requestCode);
void shareNoAppAvailable(int requestCode);
void shareError(int requestCode, QString message);
void fileUrlReceived(QString url);
void fileReceivedAndSaved(QString url);
public:
PlatformShareUtils(QObject *parent = 0);
virtual ~PlatformShareUtils();
virtual bool checkMimeTypeView(const QString &mimeType);
virtual bool checkMimeTypeEdit(const QString &mimeType);
virtual void shareText(const QString &text);
virtual void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
virtual void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
virtual void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
virtual void checkPendingIntents(const QString workingDirPath);
};
#endif // PLATFORMSHAREUTILS_H

60
headers/shareUtils/shareutils.h Executable file
View file

@ -0,0 +1,60 @@
// (c) 2017 Ekkehard Gentz (ekke)
// this project is based on ideas from
// http://blog.lasconic.com/share-on-ios-and-android-using-qml/
// see github project https://github.com/lasconic/ShareUtils-QML
// also inspired by:
// https://www.androidcode.ninja/android-share-intent-example/
// https://www.calligra.org/blogs/sharing-with-qt-on-android/
// https://stackoverflow.com/questions/7156932/open-file-in-another-app
// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData
// see also /COPYRIGHT and /LICENSE
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
// my blog about Qt for mobile: http://j.mp/qt-x
// see also /COPYRIGHT and /LICENSE
#ifndef SHAREUTILS_H
#define SHAREUTILS_H
#include <QObject>
#include <QDebug>
#include "shareUtils/platformshareutils.h"
class ShareUtils : public QObject
{
Q_OBJECT
signals:
void shareEditDone(int requestCode);
void shareFinished(int requestCode);
void shareNoAppAvailable(int requestCode);
void shareError(int requestCode, QString message);
void fileUrlReceived(QString url);
void fileReceivedAndSaved(QString url);
public slots:
void onShareEditDone(int requestCode);
void onShareFinished(int requestCode);
void onShareNoAppAvailable(int requestCode);
void onShareError(int requestCode, QString message);
void onFileUrlReceived(QString url);
void onFileReceivedAndSaved(QString url);
public:
explicit ShareUtils(QObject *parent = 0);
Q_INVOKABLE bool checkMimeTypeView(const QString &mimeType);
Q_INVOKABLE bool checkMimeTypeEdit(const QString &mimeType);
Q_INVOKABLE void shareText(const QString &text);
Q_INVOKABLE void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
Q_INVOKABLE void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
Q_INVOKABLE void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
Q_INVOKABLE void checkPendingIntents(const QString workingDirPath);
private:
PlatformShareUtils* mPlatformShareUtils;
};
#endif //SHAREUTILS_H

View file

@ -7,7 +7,6 @@ Dialog {
property var dataObj property var dataObj
property string subTitle: "" property string subTitle: ""
property int implicitY: parent.height - implicitHeight
signal selectionFinished(int index, var data) signal selectionFinished(int index, var data)

View file

@ -0,0 +1,43 @@
import QtQuick 2.0
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
Dialog {
id: control
property string _shareUrl
parent: Overlay.overlay
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
modal: true
title: "Share these results"
contentItem: RowLayout {
Repeater {
id: buttonRepeater
property var buttons: [
["\uf0c1", "Link", serverConn.shareResultsAsUrl],
["\uf029", "QR-code", null],
["\uf1c1", "Poster", serverConn.shareResultsAsPoster],
]
model: buttons
delegate: Button {
flat: true
font.family: fa5solid.name
text: "<font size=\"+4\">" + modelData[0] + "</font><br><br> " + modelData[1] + " "
onClicked: buttonRepeater.buttons[index][2](_shareUrl)
}
}
}
function appear(shareUrl) {
_shareUrl = shareUrl
control.open()
}
}

View file

@ -174,6 +174,19 @@ Page {
return widgetType return widgetType
} }
function encodeQueryData(data) {
const ret = [];
for (let d in data)
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
return ret.join('&');
}
function shareWidget() {
var url = "https://l.bluerock.dev/?" + encodeQueryData(params)
sharePu.appear(url)
console.log("Url will be:", url)
}
Loader { Loader {
id: widgetLd id: widgetLd
@ -207,9 +220,6 @@ Page {
delete(widgetLd.sourceComponent) delete(widgetLd.sourceComponent)
return false return false
} }
//
} }
function getFile(widgetType) { function getFile(widgetType) {
@ -295,4 +305,10 @@ Page {
} }
} }
} }
SharePopup {
id: sharePu
Material.theme: root.Material.theme
}
} }

View file

@ -65,6 +65,15 @@ DataListView {
text: "\uf0e8" text: "\uf0e8"
font.family: fa5solid.name font.family: fa5solid.name
} }
ToolButton {
id: shareToolBt
onClicked: shareWidget()
text: "\uf1e0"
font.family: fa5solid.name
}
} }
property var widgetData: currentWidgetData property var widgetData: currentWidgetData

View file

@ -132,9 +132,8 @@ Window {
//app.openAthlete() // dorian: 53139 , rustam: 6933 , helen: 53300 //app.openAthlete() // dorian: 53139 , rustam: 6933 , helen: 53300
//openWidget({nation:'GER'}) //openWidget({nation:'GER'})
//mainStack.push("Pages/AthleteSearchPage.qml") //mainStack.push("Pages/AthleteSearchPage.qml")
//openWidget({comp: 11651, cat: 26}) openWidget({comp: 11651, cat: 26})
//openWidget({person: 6623}) //openWidget({person: 6623})
openWidget({cat: 35, comp: 11181, type: "starters"})
} }
FontLoader { FontLoader {

View file

@ -28,5 +28,6 @@
<file>Components/DisclaimerDialog.qml</file> <file>Components/DisclaimerDialog.qml</file>
<file>Components/ColoredItemDelegate.qml</file> <file>Components/ColoredItemDelegate.qml</file>
<file>Components/AlignedButton.qml</file> <file>Components/AlignedButton.qml</file>
<file>Components/SharePopup.qml</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

View file

@ -20,5 +20,6 @@
<file>icons/sac-dark.png</file> <file>icons/sac-dark.png</file>
<file>fonts/fa5regular.otf</file> <file>fonts/fa5regular.otf</file>
<file>fonts/fa5solid.otf</file> <file>fonts/fa5solid.otf</file>
<file>PosterTemplate.png</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -20,6 +20,8 @@
BlueRockBackend::BlueRockBackend(QObject *parent) : QObject(parent) BlueRockBackend::BlueRockBackend(QObject *parent) : QObject(parent)
{ {
this->_shareUtils = new ShareUtils(this);
this->shareResultsAsPoster("test");
} }
QVariant BlueRockBackend::getWidgetData(QVariantMap params) { QVariant BlueRockBackend::getWidgetData(QVariantMap params) {
@ -79,6 +81,23 @@ QVariantMap BlueRockBackend::getParamsFromUrl(QString stringUrl) {
return params; return params;
} }
void BlueRockBackend::shareResultsAsUrl(QString url) {
this->_shareUtils->shareText(url);
}
void BlueRockBackend::shareResultsAsPoster(QString url) {
QPdfWriter writer("/tmp/test.pdf");
writer.setPageSize(QPageSize(QPageSize::A4));
writer.setPageMargins(QMargins(0, 0, 0, 0));
writer.setResolution(600);
QPainter painter(&writer);
painter.drawText(QRect(0, 0, 1980, 100),Qt::AlignHCenter|Qt::AlignBottom,
"Children's Health Checkup Form");
QPixmap image(":/PosterTemplate.png");
painter.drawPixmap(0,0, writer.width(), writer.height(), image);
painter.end();
}
// ------------------------ // ------------------------
// --- Helper functions --- // --- Helper functions ---
// ------------------------ // ------------------------

View file

@ -0,0 +1,348 @@
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
// my blog about Qt for mobile: http://j.mp/qt-x
// see also /COPYRIGHT and /LICENSE
#include "shareUtils/androidshareutils.h"
#include <QUrl>
#include <QFileInfo>
#include <QDateTime>
#include <QtAndroidExtras/QAndroidJniObject>
#include <jni.h>
const static int RESULT_OK = -1;
const static int RESULT_CANCELED = 0;
AndroidShareUtils* AndroidShareUtils::mInstance = NULL;
AndroidShareUtils::AndroidShareUtils(QObject* parent) : PlatformShareUtils(parent)
{
// we need the instance for JNI Call
mInstance = this;
}
AndroidShareUtils* AndroidShareUtils::getInstance()
{
if (!mInstance) {
mInstance = new AndroidShareUtils;
qWarning() << "AndroidShareUtils should be instantiated !";
}
return mInstance;
}
bool AndroidShareUtils::checkMimeTypeView(const QString &mimeType)
{
QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType);
jboolean verified = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"checkMimeTypeView",
"(Ljava/lang/String;)Z",
jsMime.object<jstring>());
qDebug() << "View VERIFIED: " << mimeType << " - " << verified;
return verified;
}
bool AndroidShareUtils::checkMimeTypeEdit(const QString &mimeType)
{
QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType);
jboolean verified = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"checkMimeTypeEdit",
"(Ljava/lang/String;)Z",
jsMime.object<jstring>());
qDebug() << "Edit VERIFIED: " << mimeType << " - " << verified;
return verified;
}
void AndroidShareUtils::shareText(const QString &text)
{
QAndroidJniObject jsText = QAndroidJniObject::fromString(text);
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"shareText",
"(Ljava/lang/String;)Z",
jsText.object<jstring>());
if(!ok) {
qWarning() << "Unable to resolve activity from Java";
emit shareNoAppAvailable(0);
}
}
/*
* As default we're going the Java - way with one simple JNI call (recommended)
* if altImpl is true we're going the pure JNI way
*
* If a requestId was set we want to get the Activity Result back (recommended)
* We need the Request Id and Result Id to control our workflow
*/
void AndroidShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
mIsEditMode = false;
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"sendFile",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z",
jsPath.object<jstring>(), jsTitle.object<jstring>(), jsMimeType.object<jstring>(), requestId);
if(!ok) {
qWarning() << "Unable to resolve activity from Java";
emit shareNoAppAvailable(requestId);
}
}
/*
* As default we're going the Java - way with one simple JNI call (recommended)
* if altImpl is true we're going the pure JNI way
*
* If a requestId was set we want to get the Activity Result back (recommended)
* We need the Request Id and Result Id to control our workflow
*/
void AndroidShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
mIsEditMode = false;
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"viewFile",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z",
jsPath.object<jstring>(), jsTitle.object<jstring>(), jsMimeType.object<jstring>(), requestId);
if(!ok) {
qWarning() << "Unable to resolve activity from Java";
emit shareNoAppAvailable(requestId);
}
}
/*
* As default we're going the Java - way with one simple JNI call (recommended)
* if altImpl is true we're going the pure JNI way
*
* If a requestId was set we want to get the Activity Result back (recommended)
* We need the Request Id and Result Id to control our workflow
*/
void AndroidShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
mIsEditMode = true;
mCurrentFilePath = filePath;
QFileInfo fileInfo = QFileInfo(mCurrentFilePath);
mLastModified = fileInfo.lastModified().toSecsSinceEpoch();
qDebug() << "LAST MODIFIED: " << mLastModified;
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"editFile",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z",
jsPath.object<jstring>(), jsTitle.object<jstring>(), jsMimeType.object<jstring>(), requestId);
if(!ok) {
qWarning() << "Unable to resolve activity from Java";
emit shareNoAppAvailable(requestId);
}
}
// used from QAndroidActivityResultReceiver
void AndroidShareUtils::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data)
{
Q_UNUSED(data);
qDebug() << "From JNI QAndroidActivityResultReceiver: " << receiverRequestCode << "ResultCode:" << resultCode;
processActivityResult(receiverRequestCode, resultCode);
}
// used from Activity.java onActivityResult()
void AndroidShareUtils::onActivityResult(int requestCode, int resultCode)
{
qDebug() << "From Java Activity onActivityResult: " << requestCode << "ResultCode:" << resultCode;
processActivityResult(requestCode, resultCode);
}
void AndroidShareUtils::processActivityResult(int requestCode, int resultCode)
{
// we're getting RESULT_OK only if edit is done
if(resultCode == RESULT_OK) {
emit shareEditDone(requestCode);
} else if(resultCode == RESULT_CANCELED) {
if(mIsEditMode) {
// Attention: not all Apps will give you the correct ResultCode:
// Google Fotos will send OK if saved and CANCELED if canceled
// Some Apps always sends CANCELED even if you modified and Saved the File
// so you should check the modified Timestamp of the File to know if
// you should emit shareEditDone() or shareFinished() !!!
QFileInfo fileInfo = QFileInfo(mCurrentFilePath);
qint64 currentModified = fileInfo.lastModified().toSecsSinceEpoch();
qDebug() << "CURRENT MODIFIED: " << currentModified;
if(currentModified > mLastModified) {
emit shareEditDone(requestCode);
return;
}
}
emit shareFinished(requestCode);
} else {
qDebug() << "wrong result code: " << resultCode << " from request: " << requestCode;
emit shareError(requestCode, tr("Share: an Error occured"));
}
}
void AndroidShareUtils::checkPendingIntents(const QString workingDirPath)
{
QAndroidJniObject activity = QtAndroid::androidActivity();
if(activity.isValid()) {
// create a Java String for the Working Dir Path
QAndroidJniObject jniWorkingDir = QAndroidJniObject::fromString(workingDirPath);
if(!jniWorkingDir.isValid()) {
qWarning() << "QAndroidJniObject jniWorkingDir not valid.";
emit shareError(0, tr("Share: an Error occured\nWorkingDir not valid"));
return;
}
activity.callMethod<void>("checkPendingIntents","(Ljava/lang/String;)V", jniWorkingDir.object<jstring>());
qDebug() << "checkPendingIntents: " << workingDirPath;
return;
}
qDebug() << "checkPendingIntents: Activity not valid";
}
void AndroidShareUtils::setFileUrlReceived(const QString &url)
{
if(url.isEmpty()) {
qWarning() << "setFileUrlReceived: we got an empty URL";
emit shareError(0, tr("Empty URL received"));
return;
}
qDebug() << "AndroidShareUtils setFileUrlReceived: we got the File URL from JAVA: " << url;
QString myUrl;
if(url.startsWith("file://")) {
myUrl= url.right(url.length()-7);
qDebug() << "QFile needs this URL: " << myUrl;
} else {
myUrl= url;
}
// check if File exists
QFileInfo fileInfo = QFileInfo(myUrl);
if(fileInfo.exists()) {
emit fileUrlReceived(myUrl);
} else {
qDebug() << "setFileUrlReceived: FILE does NOT exist ";
emit shareError(0, tr("File does not exist: %1").arg(myUrl));
}
}
void AndroidShareUtils::setFileReceivedAndSaved(const QString &url)
{
if(url.isEmpty()) {
qWarning() << "setFileReceivedAndSaved: we got an empty URL";
emit shareError(0, tr("Empty URL received"));
return;
}
qDebug() << "AndroidShareUtils setFileReceivedAndSaved: we got the File URL from JAVA: " << url;
QString myUrl;
if(url.startsWith("file://")) {
myUrl= url.right(url.length()-7);
qDebug() << "QFile needs this URL: " << myUrl;
} else {
myUrl= url;
}
// check if File exists
QFileInfo fileInfo = QFileInfo(myUrl);
if(fileInfo.exists()) {
emit fileReceivedAndSaved(myUrl);
} else {
qDebug() << "setFileReceivedAndSaved: FILE does NOT exist ";
emit shareError(0, tr("File does not exist: %1").arg(myUrl));
}
}
// to be safe we check if a File Url from java really exists for Qt
// if not on the Java side we'll try to read the content as Stream
bool AndroidShareUtils::checkFileExits(const QString &url)
{
if(url.isEmpty()) {
qWarning() << "checkFileExits: we got an empty URL";
emit shareError(0, tr("Empty URL received"));
return false;
}
qDebug() << "AndroidShareUtils checkFileExits: we got the File URL from JAVA: " << url;
QString myUrl;
if(url.startsWith("file://")) {
myUrl= url.right(url.length()-7);
qDebug() << "QFile needs this URL: " << myUrl;
} else {
myUrl= url;
}
// check if File exists
QFileInfo fileInfo = QFileInfo(myUrl);
if(fileInfo.exists()) {
qDebug() << "Yep: the File exists for Qt";
return true;
} else {
qDebug() << "Uuups: FILE does NOT exist ";
return false;
}
}
// instead of defining all JNICALL as demonstrated below
// there's another way, making it easier to manage all the methods
// see https://www.kdab.com/qt-android-episode-5/
#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;
}
JNIEXPORT bool JNICALL
Java_org_ekkescorner_examples_sharex_QShareActivity_checkFileExits(JNIEnv *env,
jobject obj,
jstring url)
{
const char *urlStr = env->GetStringUTFChars(url, NULL);
Q_UNUSED (obj)
bool exists = AndroidShareUtils::getInstance()->checkFileExits(urlStr);
env->ReleaseStringUTFChars(url, urlStr);
return exists;
}
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;
}
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,35 @@
#include "shareUtils/platformshareutils.h"
PlatformShareUtils::PlatformShareUtils(QObject *parent) : QObject(parent)
{
}
PlatformShareUtils::~PlatformShareUtils() {
}
bool PlatformShareUtils::checkMimeTypeView(const QString &mimeType) {
qDebug() << "check view for " << mimeType;
return true;
}
bool PlatformShareUtils::checkMimeTypeEdit(const QString &mimeType) {
qDebug() << "check edit for " << mimeType;
return true;
}
void PlatformShareUtils::shareText(const QString &text) {
qDebug() << text;
}
void PlatformShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) {
qDebug() << filePath << " - " << title << "requestId " << requestId << " - " << mimeType << "altImpl? ";
}
void PlatformShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) {
qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? ";
}
void PlatformShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) {
qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? ";
}
void PlatformShareUtils::checkPendingIntents(const QString workingDirPath) {
qDebug() << "checkPendingIntents " << workingDirPath;
}

111
sources/shareUtils/shareutils.cpp Executable file
View file

@ -0,0 +1,111 @@
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
// my blog about Qt for mobile: http://j.mp/qt-x
// see also /COPYRIGHT and /LICENSE
#include "shareUtils/shareutils.h"
#ifdef Q_OS_IOS
#include "cpp/ios/iosshareutils.hpp"
#endif
#ifdef Q_OS_ANDROID
#include "shareUtils/androidshareutils.h"
#endif
ShareUtils::ShareUtils(QObject *parent)
: QObject(parent)
{
#if defined(Q_OS_IOS)
mPlatformShareUtils = new IosShareUtils(this);
#elif defined(Q_OS_ANDROID)
mPlatformShareUtils = new AndroidShareUtils(this);
#else
mPlatformShareUtils = new PlatformShareUtils(this);
#endif
bool connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareEditDone, this, &ShareUtils::onShareEditDone);
Q_ASSERT(connectResult);
connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareFinished, this, &ShareUtils::onShareFinished);
Q_ASSERT(connectResult);
connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareNoAppAvailable, this, &ShareUtils::onShareNoAppAvailable);
Q_ASSERT(connectResult);
connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareError, this, &ShareUtils::onShareError);
Q_ASSERT(connectResult);
connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::fileUrlReceived, this, &ShareUtils::onFileUrlReceived);
Q_ASSERT(connectResult);
connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::fileReceivedAndSaved, this, &ShareUtils::onFileReceivedAndSaved);
Q_ASSERT(connectResult);
Q_UNUSED(connectResult);
}
bool ShareUtils::checkMimeTypeView(const QString &mimeType)
{
return mPlatformShareUtils->checkMimeTypeView(mimeType);
}
bool ShareUtils::checkMimeTypeEdit(const QString &mimeType)
{
return mPlatformShareUtils->checkMimeTypeEdit(mimeType);
}
void ShareUtils::shareText(const QString &text)
{
mPlatformShareUtils->shareText(text);
}
void ShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
mPlatformShareUtils->sendFile(filePath, title, mimeType, requestId);
}
void ShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
mPlatformShareUtils->viewFile(filePath, title, mimeType, requestId);
}
void ShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
mPlatformShareUtils->editFile(filePath, title, mimeType, requestId);
}
void ShareUtils::checkPendingIntents(const QString workingDirPath)
{
mPlatformShareUtils->checkPendingIntents(workingDirPath);
}
void ShareUtils::onShareEditDone(int requestCode)
{
emit shareEditDone(requestCode);
}
void ShareUtils::onShareFinished(int requestCode)
{
emit shareFinished(requestCode);
}
void ShareUtils::onShareNoAppAvailable(int requestCode)
{
emit shareNoAppAvailable(requestCode);
}
void ShareUtils::onShareError(int requestCode, QString message)
{
emit shareError(requestCode, message);
}
void ShareUtils::onFileUrlReceived(QString url)
{
emit fileUrlReceived(url);
}
void ShareUtils::onFileReceivedAndSaved(QString url)
{
emit fileReceivedAndSaved(url);
}