app/android/src/org/ekkescorner/utils/QShareUtils.java
Dorian Zedler 447c1135dd
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Fix: send file on android api 30 (fixes #32)
2022-08-11 12:44:39 +02:00

397 lines
17 KiB
Java
Executable file

// (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);
QtNative.activity().startActivity(Intent.createChooser(sendIntent, null));
return true;
}
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;
}
}