diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 6c2852d..65d57e2 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -10,7 +10,7 @@ - + @@ -68,8 +68,24 @@ --> + + + + + + + + + + + + + + + + diff --git a/android/build.gradle b/android/build.gradle index 0051ff0..de92470 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -18,6 +18,7 @@ apply plugin: 'com.android.application' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + compile 'com.android.support:support-v4:25.3.1' } android { @@ -77,5 +78,6 @@ android { lintOptions { checkReleaseBuilds false + abortOnError false } } diff --git a/android/res/xml/filepaths.xml b/android/res/xml/filepaths.xml new file mode 100644 index 0000000..6b3e3f8 --- /dev/null +++ b/android/res/xml/filepaths.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/src/de/itsblue/blueROCK/MainActivity.java b/android/src/de/itsblue/blueROCK/MainActivity.java new file mode 100755 index 0000000..24d2923 --- /dev/null +++ b/android/src/de/itsblue/blueROCK/MainActivity.java @@ -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 diff --git a/android/src/org/ekkescorner/utils/QSharePathResolver.java b/android/src/org/ekkescorner/utils/QSharePathResolver.java new file mode 100644 index 0000000..f93d1a8 --- /dev/null +++ b/android/src/org/ekkescorner/utils/QSharePathResolver.java @@ -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()); + } + +} diff --git a/android/src/org/ekkescorner/utils/QShareUtils.java b/android/src/org/ekkescorner/utils/QShareUtils.java new file mode 100755 index 0000000..38a07a7 --- /dev/null +++ b/android/src/org/ekkescorner/utils/QShareUtils.java @@ -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 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() { + @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 targetedIntents = new ArrayList(); + // 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; + } + +} diff --git a/blueROCK.pro b/blueROCK.pro index 5708afc..a5c666b 100644 --- a/blueROCK.pro +++ b/blueROCK.pro @@ -1,4 +1,4 @@ -QT += quick qml quickcontrols2 purchasing widgets +QT += quick qml quickcontrols2 purchasing printsupport CONFIG += c++11 VERSION = 0.5.0 @@ -15,10 +15,23 @@ DEFINES += QT_DEPRECATED_WARNINGS # 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 +INCLUDEPATH *= $$PWD/headers + +# Add version to define +DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\" + SOURCES += \ + sources/shareUtils/platformshareutils.cpp \ sources/appsettings.cpp \ sources/bluerockbackend.cpp \ - sources/main.cpp + sources/shareUtils/shareutils.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/shared/shared.qrc \ @@ -37,27 +50,28 @@ qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target -# Add version to define -DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\" - -HEADERS += \ - headers/appsettings.h \ - headers/bluerockbackend.h - DISTFILES += \ - CHANGELOG.md \ - android/AndroidManifest.xml \ - android/build.gradle \ - android/gradle.properties \ - android/gradle/wrapper/gradle-wrapper.jar \ - android/gradle/wrapper/gradle-wrapper.properties \ - android/gradlew \ - android/gradlew.bat \ - android/res/values/libs.xml + 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/build.gradle \ + android/gradle.properties \ + android/gradle/wrapper/gradle-wrapper.jar \ + android/gradle/wrapper/gradle-wrapper.properties \ + android/gradlew \ + android/gradlew.bat \ + android/res/values/libs.xml \ + android/res/xml/filepaths.xml + defineReplace(droidVersionCode) { segments = $$split(1, ".") for (segment, segments): vCode = "$$first(vCode)$$format_number($$segment, width=3 zeropad)" diff --git a/headers/bluerockbackend.h b/headers/bluerockbackend.h index 091b9be..b03b146 100644 --- a/headers/bluerockbackend.h +++ b/headers/bluerockbackend.h @@ -24,6 +24,17 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shareUtils/shareutils.h" class BlueRockBackend : public QObject { @@ -34,13 +45,16 @@ public: private: QVariantMap senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery()); + ShareUtils* _shareUtils; + signals: public slots: QVariant getWidgetData(QVariantMap params); - QVariantMap getParamsFromUrl(QString url); + void shareResultsAsUrl(QString url); + void shareResultsAsPoster(QString url); }; diff --git a/headers/shareUtils/androidshareutils.h b/headers/shareUtils/androidshareutils.h new file mode 100755 index 0000000..50786fd --- /dev/null +++ b/headers/shareUtils/androidshareutils.h @@ -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 +#include + +#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 diff --git a/headers/shareUtils/platformshareutils.h b/headers/shareUtils/platformshareutils.h new file mode 100644 index 0000000..504983f --- /dev/null +++ b/headers/shareUtils/platformshareutils.h @@ -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 +#include + +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 diff --git a/headers/shareUtils/shareutils.h b/headers/shareUtils/shareutils.h new file mode 100755 index 0000000..956a316 --- /dev/null +++ b/headers/shareUtils/shareutils.h @@ -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 +#include + +#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 diff --git a/resources/qml/Components/SelectorPopup.qml b/resources/qml/Components/SelectorPopup.qml index b598dd5..6cdf282 100644 --- a/resources/qml/Components/SelectorPopup.qml +++ b/resources/qml/Components/SelectorPopup.qml @@ -7,7 +7,6 @@ Dialog { property var dataObj property string subTitle: "" - property int implicitY: parent.height - implicitHeight signal selectionFinished(int index, var data) diff --git a/resources/qml/Components/SharePopup.qml b/resources/qml/Components/SharePopup.qml new file mode 100644 index 0000000..be10465 --- /dev/null +++ b/resources/qml/Components/SharePopup.qml @@ -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: "" + modelData[0] + "

" + modelData[1] + " " + onClicked: buttonRepeater.buttons[index][2](_shareUrl) + } + } + } + + function appear(shareUrl) { + _shareUrl = shareUrl + control.open() + } +} diff --git a/resources/qml/Pages/WidgetPage.qml b/resources/qml/Pages/WidgetPage.qml index 651dadd..cd9ac93 100644 --- a/resources/qml/Pages/WidgetPage.qml +++ b/resources/qml/Pages/WidgetPage.qml @@ -174,6 +174,19 @@ Page { 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 { id: widgetLd @@ -207,9 +220,6 @@ Page { delete(widgetLd.sourceComponent) return false } - - - // } function getFile(widgetType) { @@ -295,4 +305,10 @@ Page { } } } + + SharePopup { + id: sharePu + + Material.theme: root.Material.theme + } } diff --git a/resources/qml/Widgets/ResultWidget.qml b/resources/qml/Widgets/ResultWidget.qml index 77e85c4..9a07249 100644 --- a/resources/qml/Widgets/ResultWidget.qml +++ b/resources/qml/Widgets/ResultWidget.qml @@ -65,6 +65,15 @@ DataListView { text: "\uf0e8" font.family: fa5solid.name } + + ToolButton { + id: shareToolBt + + onClicked: shareWidget() + + text: "\uf1e0" + font.family: fa5solid.name + } } property var widgetData: currentWidgetData diff --git a/resources/qml/main.qml b/resources/qml/main.qml index 063b5fa..b60c655 100644 --- a/resources/qml/main.qml +++ b/resources/qml/main.qml @@ -132,9 +132,8 @@ Window { //app.openAthlete() // dorian: 53139 , rustam: 6933 , helen: 53300 //openWidget({nation:'GER'}) //mainStack.push("Pages/AthleteSearchPage.qml") - //openWidget({comp: 11651, cat: 26}) + openWidget({comp: 11651, cat: 26}) //openWidget({person: 6623}) - openWidget({cat: 35, comp: 11181, type: "starters"}) } FontLoader { diff --git a/resources/qml/qml.qrc b/resources/qml/qml.qrc index 13a690e..2d4373e 100644 --- a/resources/qml/qml.qrc +++ b/resources/qml/qml.qrc @@ -28,5 +28,6 @@ Components/DisclaimerDialog.qml Components/ColoredItemDelegate.qml Components/AlignedButton.qml + Components/SharePopup.qml diff --git a/resources/shared/PosterTemplate.png b/resources/shared/PosterTemplate.png new file mode 100644 index 0000000..74b8721 Binary files /dev/null and b/resources/shared/PosterTemplate.png differ diff --git a/resources/shared/PosterTemplate.xcf b/resources/shared/PosterTemplate.xcf new file mode 100644 index 0000000..45fbaae Binary files /dev/null and b/resources/shared/PosterTemplate.xcf differ diff --git a/resources/shared/shared.qrc b/resources/shared/shared.qrc index c4d820c..552fa12 100644 --- a/resources/shared/shared.qrc +++ b/resources/shared/shared.qrc @@ -20,5 +20,6 @@ icons/sac-dark.png fonts/fa5regular.otf fonts/fa5solid.otf + PosterTemplate.png diff --git a/sources/bluerockbackend.cpp b/sources/bluerockbackend.cpp index b4f9ef8..c81f6dd 100644 --- a/sources/bluerockbackend.cpp +++ b/sources/bluerockbackend.cpp @@ -20,6 +20,8 @@ BlueRockBackend::BlueRockBackend(QObject *parent) : QObject(parent) { + this->_shareUtils = new ShareUtils(this); + this->shareResultsAsPoster("test"); } QVariant BlueRockBackend::getWidgetData(QVariantMap params) { @@ -79,6 +81,23 @@ QVariantMap BlueRockBackend::getParamsFromUrl(QString stringUrl) { 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 --- // ------------------------ diff --git a/sources/shareUtils/androidshareutils.cpp b/sources/shareUtils/androidshareutils.cpp new file mode 100755 index 0000000..8390008 --- /dev/null +++ b/sources/shareUtils/androidshareutils.cpp @@ -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 +#include +#include + +#include +#include + +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("org/ekkescorner/utils/QShareUtils", + "checkMimeTypeView", + "(Ljava/lang/String;)Z", + jsMime.object()); + qDebug() << "View VERIFIED: " << mimeType << " - " << verified; + return verified; +} + +bool AndroidShareUtils::checkMimeTypeEdit(const QString &mimeType) +{ + QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType); + jboolean verified = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "checkMimeTypeEdit", + "(Ljava/lang/String;)Z", + jsMime.object()); + qDebug() << "Edit VERIFIED: " << mimeType << " - " << verified; + return verified; +} + +void AndroidShareUtils::shareText(const QString &text) +{ + QAndroidJniObject jsText = QAndroidJniObject::fromString(text); + jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "shareText", + "(Ljava/lang/String;)Z", + jsText.object()); + + 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("org/ekkescorner/utils/QShareUtils", + "sendFile", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", + jsPath.object(), jsTitle.object(), jsMimeType.object(), 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("org/ekkescorner/utils/QShareUtils", + "viewFile", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", + jsPath.object(), jsTitle.object(), jsMimeType.object(), 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("org/ekkescorner/utils/QShareUtils", + "editFile", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", + jsPath.object(), jsTitle.object(), jsMimeType.object(), 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("checkPendingIntents","(Ljava/lang/String;)V", jniWorkingDir.object()); + 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 diff --git a/sources/shareUtils/platformshareutils.cpp b/sources/shareUtils/platformshareutils.cpp new file mode 100644 index 0000000..c90793a --- /dev/null +++ b/sources/shareUtils/platformshareutils.cpp @@ -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; +} diff --git a/sources/shareUtils/shareutils.cpp b/sources/shareUtils/shareutils.cpp new file mode 100755 index 0000000..effb0f1 --- /dev/null +++ b/sources/shareUtils/shareutils.cpp @@ -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); +} +