mirror of
https://github.com/cagnulein/qdomyos-zwift.git
synced 2026-02-18 00:17:41 +01:00
Compare commits
12 Commits
Computrain
...
share_send
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c9b85c6e5 | ||
|
|
52f16e8410 | ||
|
|
8a00025e30 | ||
|
|
c5128eef0a | ||
|
|
a3a67ae14b | ||
|
|
9bb93781d3 | ||
|
|
b7e74575f6 | ||
|
|
de320977d2 | ||
|
|
e17c44bd80 | ||
|
|
d877556f47 | ||
|
|
43e72bb876 | ||
|
|
815b94809a |
6
src/Constants.qml
Normal file
6
src/Constants.qml
Normal file
@@ -0,0 +1,6 @@
|
||||
pragma Singleton
|
||||
import QtQuick 2.6
|
||||
|
||||
QtObject {
|
||||
readonly property int SHARE_REQUEST_CODE: 131313
|
||||
}
|
||||
@@ -5,12 +5,26 @@
|
||||
<!-- %%INSERT_PERMISSIONS -->
|
||||
|
||||
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
|
||||
Remove the comment if you do not require these default features. -->
|
||||
<!-- %%INSERT_FEATURES -->
|
||||
Remove the comment if you do not require these default features. -->
|
||||
<!-- %%INSERT_FEATURES -->
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.EDIT"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||
<application android:hardwareAccelerated="true" android:debuggable="false" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="qdomyos-zwift" android:extractNativeLibs="true" android:icon="@drawable/icon" android:usesCleartextTraffic="true">
|
||||
<activity android:theme="@style/Theme.AppCompat" android:exported="true" 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="QZ" android:launchMode="singleTop">
|
||||
<activity android:theme="@style/Theme.AppCompat" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.cagnulen.qdomyoszwift.share_send_recv.QShareActivity" android:label="QZ" android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
@@ -74,6 +88,18 @@
|
||||
-->
|
||||
<meta-data android:name="android.app.extract_android_style" android:value="default"/>
|
||||
<!-- extract android style -->
|
||||
<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>
|
||||
<service
|
||||
android:name=".ForegroundService"
|
||||
@@ -108,7 +134,10 @@
|
||||
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||
android:value="ocr" />
|
||||
|
||||
<activity android:name="org.cagnulen.qdomyoszwift.MyActivity" />
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
<provider android:name="android.support.v4.content.FileProvider" android:authorities="org.cagnulen.qdomyoszwift.share_send_recv.fileprovider" android:grantUriPermissions="true" android:exported="false">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
|
||||
</provider>
|
||||
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
|
||||
</application>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies {
|
||||
|
||||
def appcompat_version = "1.3.1"
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation 'com.android.support:support-v4:25.3.1'
|
||||
implementation "com.android.billingclient:billing:5.0.0"
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
|
||||
|
||||
3
src/android/res/xml/filepaths.xml
Normal file
3
src/android/res/xml/filepaths.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="settings_shared" path="settings/" />
|
||||
</paths>
|
||||
250
src/android/src/org/cagnulen/qdomyoszwift/share_send_recv/QShareActivity.java
Executable file
250
src/android/src/org/cagnulen/qdomyoszwift/share_send_recv/QShareActivity.java
Executable file
@@ -0,0 +1,250 @@
|
||||
// (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 org.cagnulen.qdomyoszwift.share_send_recv;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
|
||||
public class QShareActivity 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;
|
||||
public static final String TAG = "QZQShareActivity";
|
||||
|
||||
// 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(TAG, "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(TAG, "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(TAG, "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(TAG, "onActivityResult requestCode: "+requestCode);
|
||||
if (resultCode == RESULT_OK) {
|
||||
Log.d(TAG, "onActivityResult - resultCode: SUCCESS");
|
||||
} else {
|
||||
Log.d(TAG, "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);
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
// if we are opened from other apps:
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
Log.d(TAG, "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
|
||||
|
||||
private boolean checkFileExistsJava(String path) {
|
||||
if (path.startsWith("file://"))
|
||||
path = path.substring(7);
|
||||
File f = new File(path);
|
||||
return f.exists() && f.isFile() && f.canRead();
|
||||
}
|
||||
|
||||
public void checkPendingIntents(String workingDir) {
|
||||
isInitialized = true;
|
||||
workingDirPath = workingDir;
|
||||
Log.d(TAG, workingDirPath);
|
||||
if(isIntentPending) {
|
||||
isIntentPending = false;
|
||||
Log.d(TAG, "checkPendingIntents: true");
|
||||
processIntent();
|
||||
} else {
|
||||
Log.d(TAG, "nothingPending");
|
||||
}
|
||||
} // checkPendingIntents
|
||||
|
||||
// process the Intent if Action is SEND or VIEW
|
||||
private void processIntent(){
|
||||
Intent intent = getIntent();
|
||||
|
||||
Uri intentUri;
|
||||
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(TAG, "Intent unknown action: " + intent.getAction());
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "action: " + intentAction);
|
||||
processIncomingUri(intentUri);
|
||||
}
|
||||
|
||||
public void processIncomingUri(String intentUri, String workingDir) {
|
||||
workingDirPath = workingDir;
|
||||
Log.d(TAG, workingDirPath);
|
||||
processIncomingUri(Uri.parse(intentUri));
|
||||
}
|
||||
|
||||
public void processIncomingUri(Uri intentUri) {
|
||||
String intentScheme;
|
||||
if (intentUri == null){
|
||||
Log.d(TAG, "Intent URI: is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Intent URI: " + intentUri.toString());
|
||||
|
||||
// content or file
|
||||
intentScheme = intentUri.getScheme();
|
||||
if (intentScheme == null){
|
||||
Log.d(TAG, "Intent URI Scheme: is null");
|
||||
return;
|
||||
}
|
||||
if(intentScheme.equals("file")){
|
||||
// URI as encoded string
|
||||
String pth = intentUri.toString();
|
||||
Log.d(TAG, "Intent File URI: " + pth + " read = " + checkFileExistsJava(pth));
|
||||
if (checkFileExits(pth)) {
|
||||
setFileUrlReceived(pth);
|
||||
// we are done Qt can deal with file scheme
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!intentScheme.equals("content")){
|
||||
Log.d(TAG, "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(TAG, "Intent Content URI: " + intentUri.toString());
|
||||
ContentResolver cR = this.getContentResolver();
|
||||
MimeTypeMap mime = MimeTypeMap.getSingleton();
|
||||
String fileExtension = mime.getExtensionFromMimeType(cR.getType(intentUri));
|
||||
Log.d(TAG,"Intent extension: "+fileExtension);
|
||||
String mimeType = cR.getType(intentUri);
|
||||
Log.d(TAG," Intent MimeType: "+mimeType);
|
||||
String name = QShareUtils.getContentName(cR, intentUri);
|
||||
if(name != null) {
|
||||
Log.d(TAG, "Intent Name:" + name);
|
||||
} else {
|
||||
Log.d(TAG, "Intent Name: is NULL");
|
||||
}
|
||||
String filePath;
|
||||
filePath = QSharePathResolver.getRealPathFromURI(this, intentUri);
|
||||
if(filePath == null) {
|
||||
Log.d(TAG, "QSharePathResolver: filePath is NULL");
|
||||
} else {
|
||||
Log.d(TAG, "QSharePathResolver:" + filePath + " read = " + checkFileExistsJava(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(TAG, "Intent FilePath: is NULL");
|
||||
return;
|
||||
}
|
||||
else
|
||||
Log.d(TAG, "Intent FilePath returned: " + filePath);
|
||||
setFileReceivedAndSaved(filePath);
|
||||
} // processIntent
|
||||
|
||||
} // class QShareActivity
|
||||
@@ -0,0 +1,224 @@
|
||||
// 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.cagnulen.qdomyoszwift.share_send_recv;
|
||||
|
||||
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 final String TAG = "QZQSharePathResolver";
|
||||
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(TAG," 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(TAG," isDownloadsDocument");
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
Log.d(TAG," 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(TAG," 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(TAG," is uri.getScheme()");
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri))
|
||||
return uri.getLastPathSegment();
|
||||
|
||||
return getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// Other Providers
|
||||
else{
|
||||
Log.d(TAG,"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(TAG,"NOT DocumentsContract.isDocumentUri");
|
||||
Log.d(TAG," is uri.getScheme()");
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri))
|
||||
return uri.getLastPathSegment();
|
||||
Log.d(TAG," return: getDataColumn ");
|
||||
return getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
Log.d(TAG,"NOT DocumentsContract.isDocumentUri");
|
||||
Log.d(TAG," 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());
|
||||
}
|
||||
|
||||
}
|
||||
399
src/android/src/org/cagnulen/qdomyoszwift/share_send_recv/QShareUtils.java
Executable file
399
src/android/src/org/cagnulen/qdomyoszwift/share_send_recv/QShareUtils.java
Executable file
@@ -0,0 +1,399 @@
|
||||
// (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.cagnulen.qdomyoszwift.share_send_recv;
|
||||
|
||||
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="org.cagnulen.qdomyoszwift.share_send_recv.fileprovider";
|
||||
public static final String TAG = "QZQShareUtils";
|
||||
|
||||
protected QShareUtils()
|
||||
{
|
||||
//Log.d(TAG, "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(TAG, "checkMime YEP - we can go on and View");
|
||||
return true;
|
||||
} else {
|
||||
Log.d(TAG, "checkMimesorry - 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(TAG, "checkMime YEP - we can go on and Edit");
|
||||
return true;
|
||||
} else {
|
||||
Log.d(TAG, "checkMimesorry - no App available to Edit");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean share(String text, String url) {
|
||||
if (QtNative.activity() == null)
|
||||
return false;
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, text + " " + url);
|
||||
sendIntent.setType("text/plain");
|
||||
|
||||
// Verify that the intent will resolve to an activity
|
||||
if (sendIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) {
|
||||
QtNative.activity().startActivity(sendIntent);
|
||||
return true;
|
||||
} else {
|
||||
Log.d(TAG, "shareIntent 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(TAG, "" + 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(TAG, "" + 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_ALL);
|
||||
if (appInfoList.isEmpty()) {
|
||||
Log.d(TAG, "" + title+" appInfoList.isEmpty");
|
||||
return false;
|
||||
}
|
||||
Log.d(TAG, "" + 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(TAG, "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(TAG, "" + title+" targetedIntents.isEmpty");
|
||||
return false;
|
||||
}
|
||||
Log.d(TAG, "" + title+" targetedIntents "+targetedIntents.size());
|
||||
|
||||
// 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(theIntent, title);
|
||||
if (targetedIntents.isEmpty()) {
|
||||
Log.d(TAG, "" + 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) {
|
||||
Log.d(TAG, "activity for result " + requestId);
|
||||
if(requestId > 0) {
|
||||
QtNative.activity().startActivityForResult(chooserIntent, requestId);
|
||||
} else {
|
||||
QtNative.activity().startActivity(chooserIntent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Log.d(TAG, "" + 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(TAG, "sendFile - cannot be shared: " + filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.d(TAG, "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(TAG, "sendFile guessed mimeType:" + mimeType);
|
||||
} else {
|
||||
Log.d(TAG, "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(TAG, "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(TAG, "viewFile from file path: " + filePath);
|
||||
Log.d(TAG, "viewFile to content URI: " + uri.toString());
|
||||
|
||||
if(mimeType == null || mimeType.isEmpty()) {
|
||||
// fallback if mimeType not set
|
||||
mimeType = QtNative.activity().getContentResolver().getType(uri);
|
||||
Log.d(TAG, "viewFile guessed mimeType:" + mimeType);
|
||||
} else {
|
||||
Log.d(TAG, "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(TAG, "editFile - cannot be shared: " + filePath);
|
||||
return false;
|
||||
}
|
||||
Log.d(TAG, "editFile" + uri.toString());
|
||||
|
||||
if(mimeType == null || mimeType.isEmpty()) {
|
||||
// fallback if mimeType not set
|
||||
mimeType = QtNative.activity().getContentResolver().getType(uri);
|
||||
Log.d(TAG, "editFile guessed mimeType:" + mimeType);
|
||||
} else {
|
||||
Log.d(TAG, "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(TAG, "- create File" + filePath);
|
||||
File f = new File(filePath);
|
||||
FileOutputStream tmp = new FileOutputStream(f);
|
||||
Log.d(TAG, "- create Filenew 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5307,6 +5307,7 @@ void homeform::trainprogram_open_clicked(const QUrl &fileName) {
|
||||
}
|
||||
|
||||
trainProgram = trainprogram::load(file.fileName(), bluetoothManager, file.fileName().right(3).toUpper());
|
||||
emit trainProgramLoaded(file.fileName(), trainProgram->rows.length());
|
||||
|
||||
QString movieName = file.fileName().left(file.fileName().length() - 3) + "mp4";
|
||||
if (QFile::exists(movieName)) {
|
||||
@@ -5380,6 +5381,7 @@ void homeform::trainprogram_zwo_loaded(const QString &s) {
|
||||
emit infoChanged(m_info);
|
||||
}
|
||||
}
|
||||
emit trainProgramLoaded(QStringLiteral("/web/json"), trainProgram->rows.length());
|
||||
}
|
||||
trainProgramSignals();
|
||||
}
|
||||
@@ -6426,6 +6428,8 @@ void homeform::saveSettings(const QUrl &filename) {
|
||||
}
|
||||
}
|
||||
}
|
||||
settings2Save.sync();
|
||||
emit settingsSaved(path, settigsAllKeys.size());
|
||||
}
|
||||
|
||||
void homeform::loadSettings(const QUrl &filename) {
|
||||
@@ -6436,8 +6440,10 @@ void homeform::loadSettings(const QUrl &filename) {
|
||||
qDebug() << "homeform::loadSettings" << file.fileName();
|
||||
|
||||
QSettings settings;
|
||||
QSettings settings2Load(file.fileName(), QSettings::IniFormat);
|
||||
QString path = filename.toLocalFile();
|
||||
QSettings settings2Load(path, QSettings::IniFormat);
|
||||
auto settings2LoadAllKeys = settings2Load.allKeys();
|
||||
qDebug() << "Will load settings from" << path << " kn = " << settings2LoadAllKeys.size();
|
||||
for (const QString &s : qAsConst(settings2LoadAllKeys)) {
|
||||
if (!s.contains(QZSettings::cryptoKeySettingsProfiles)) {
|
||||
if (!s.contains(QStringLiteral("password")) && !s.contains(QStringLiteral("token"))) {
|
||||
@@ -6449,6 +6455,7 @@ void homeform::loadSettings(const QUrl &filename) {
|
||||
}
|
||||
}
|
||||
}
|
||||
emit settingsLoaded(path, settings2LoadAllKeys.size());
|
||||
}
|
||||
|
||||
void homeform::deleteSettings(const QUrl &filename) { QFile(filename.toLocalFile()).remove(); }
|
||||
|
||||
@@ -821,6 +821,7 @@ class homeform : public QObject {
|
||||
void stopIconChanged(QString value);
|
||||
void stopColorChanged(QString value);
|
||||
void infoChanged(QString value);
|
||||
void trainProgramLoaded(QString name, int lines);
|
||||
void topBarHeightChanged(int value);
|
||||
void bluetoothDevicesChanged(QStringList value);
|
||||
void tile_orderChanged(QStringList value);
|
||||
@@ -845,6 +846,8 @@ class homeform : public QObject {
|
||||
void workoutNameChanged(QString name);
|
||||
void workoutStartDateChanged(QString name);
|
||||
void instructorNameChanged(QString name);
|
||||
void settingsSaved(QString settingsFileUri, int settingsN);
|
||||
void settingsLoaded(QString settingsFileUri, int settingsN);
|
||||
void startRequestedChanged(bool value);
|
||||
void stopRequestedChanged(bool value);
|
||||
|
||||
|
||||
@@ -98,5 +98,20 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Generic File</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
32
src/ios/share_send_recv/docviewcontroller.mm
Normal file
32
src/ios/share_send_recv/docviewcontroller.mm
Normal file
@@ -0,0 +1,32 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#import "DocViewController.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
@interface DocViewController ()
|
||||
@end
|
||||
@implementation DocViewController
|
||||
#pragma mark -
|
||||
#pragma mark View Life Cycle
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
}
|
||||
#pragma mark -
|
||||
#pragma mark Document Interaction Controller Delegate Methods
|
||||
- (UIViewController *) documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller {
|
||||
#pragma unused (controller)
|
||||
return self;
|
||||
}
|
||||
- (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller
|
||||
{
|
||||
#pragma unused (controller)
|
||||
qDebug() << "end preview";
|
||||
|
||||
self.mIosShareUtils->handleDocumentPreviewDone(self.requestId);
|
||||
|
||||
[self removeFromParentViewController];
|
||||
}
|
||||
@end
|
||||
144
src/ios/share_send_recv/iosshareutils.mm
Executable file
144
src/ios/share_send_recv/iosshareutils.mm
Executable file
@@ -0,0 +1,144 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#import "iosshareutils.hpp"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <QGuiApplication>
|
||||
#import <QQuickWindow>
|
||||
#import <QDesktopServices>
|
||||
#import <QUrl>
|
||||
#import <QFileInfo>
|
||||
|
||||
#import <UIKit/UIDocumentInteractionController.h>
|
||||
|
||||
#import "docviewcontroller.hpp"
|
||||
|
||||
IosShareUtils::IosShareUtils(QObject *parent) : PlatformShareUtils(parent)
|
||||
{
|
||||
// Sharing Files from other iOS Apps I got the ideas and some code contribution from:
|
||||
// Thomas K. Fischer (@taskfabric) - http://taskfabric.com - thx
|
||||
QDesktopServices::setUrlHandler("file", this, "handleFileUrlReceived");
|
||||
}
|
||||
|
||||
bool IosShareUtils::checkMimeTypeView(const QString &mimeType) {
|
||||
#pragma unused (mimeType)
|
||||
// dummi implementation on iOS
|
||||
// MimeType not used yet
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IosShareUtils::checkMimeTypeEdit(const QString &mimeType) {
|
||||
#pragma unused (mimeType)
|
||||
// dummi implementation on iOS
|
||||
// MimeType not used yet
|
||||
return true;
|
||||
}
|
||||
|
||||
void IosShareUtils::share(const QString &text, const QUrl &url) {
|
||||
|
||||
NSMutableArray *sharingItems = [NSMutableArray new];
|
||||
|
||||
if (!text.isEmpty()) {
|
||||
[sharingItems addObject:text.toNSString()];
|
||||
}
|
||||
if (url.isValid()) {
|
||||
[sharingItems addObject:url.toNSURL()];
|
||||
}
|
||||
|
||||
// get the main window rootViewController
|
||||
UIViewController *qtUIViewController = [[UIApplication sharedApplication].keyWindow rootViewController];
|
||||
|
||||
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
|
||||
if ( [activityController respondsToSelector:@selector(popoverPresentationController)] ) { // iOS8
|
||||
activityController.popoverPresentationController.sourceView = qtUIViewController.view;
|
||||
}
|
||||
[qtUIViewController presentViewController:activityController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
// altImpl not used yet on iOS, on Android twi ways to use JNI
|
||||
void IosShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) {
|
||||
#pragma unused (title, mimeType, altImpl)
|
||||
|
||||
NSString* nsFilePath = filePath.toNSString();
|
||||
NSURL *nsFileUrl = [NSURL fileURLWithPath:nsFilePath];
|
||||
|
||||
static DocViewController* docViewController = nil;
|
||||
if(docViewController!=nil)
|
||||
{
|
||||
[docViewController removeFromParentViewController];
|
||||
[docViewController release];
|
||||
}
|
||||
|
||||
UIDocumentInteractionController* documentInteractionController = nil;
|
||||
documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:nsFileUrl];
|
||||
|
||||
UIViewController* qtUIViewController = [[[[UIApplication sharedApplication]windows] firstObject]rootViewController];
|
||||
if(qtUIViewController!=nil)
|
||||
{
|
||||
docViewController = [[DocViewController alloc] init];
|
||||
|
||||
docViewController.requestId = requestId;
|
||||
// we need this to be able to execute handleDocumentPreviewDone() method,
|
||||
// when preview was finished
|
||||
docViewController.mIosShareUtils = this;
|
||||
|
||||
[qtUIViewController addChildViewController:docViewController];
|
||||
documentInteractionController.delegate = docViewController;
|
||||
// [documentInteractionController presentPreviewAnimated:YES];
|
||||
if(![documentInteractionController presentPreviewAnimated:YES])
|
||||
{
|
||||
emit shareError(0, tr("No App found to open: %1").arg(filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void IosShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) {
|
||||
#pragma unused (title, mimeType)
|
||||
|
||||
sendFile(filePath, title, mimeType, requestId, altImpl);
|
||||
}
|
||||
|
||||
void IosShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) {
|
||||
#pragma unused (title, mimeType)
|
||||
|
||||
sendFile(filePath, title, mimeType, requestId, altImpl);
|
||||
}
|
||||
|
||||
void IosShareUtils::handleDocumentPreviewDone(const int &requestId)
|
||||
{
|
||||
// documentInteractionControllerDidEndPreview
|
||||
qDebug() << "handleShareDone: " << requestId;
|
||||
emit shareFinished(requestId);
|
||||
}
|
||||
|
||||
void IosShareUtils::handleFileUrlReceived(const QUrl &url)
|
||||
{
|
||||
QString incomingUrl = url.toString();
|
||||
if(incomingUrl.isEmpty()) {
|
||||
qWarning() << "setFileUrlReceived: we got an empty URL";
|
||||
emit shareError(0, tr("Empty URL received"));
|
||||
return;
|
||||
}
|
||||
qDebug() << "IosShareUtils setFileUrlReceived: we got the File URL from iOS: " << incomingUrl;
|
||||
QString myUrl;
|
||||
if(incomingUrl.startsWith("file://")) {
|
||||
myUrl= incomingUrl.right(incomingUrl.length()-7);
|
||||
qDebug() << "QFile needs this URL: " << myUrl;
|
||||
} else {
|
||||
myUrl= incomingUrl;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,9 @@ double KeiserM3iDeviceSimulator::calcSpeed(keiser_m3i_out_t *f, bool pause) {
|
||||
} else {
|
||||
int acc_time = realtime - this->old_timeRms;
|
||||
double acc = realdist - this->old_dist;
|
||||
if (!pause && acc > 0 && acc_time >= 0 && (acc > 1e-6 || acc_time > 0)) {
|
||||
qDebug() << QStringLiteral("acc = ") << acc << QStringLiteral(" acc_time = ") << acc_time
|
||||
<< QStringLiteral(" pause = ") << pause;
|
||||
if (!pause && acc >= 0 && acc_time >= 0 && (acc > 1e-6 || acc_time > 0)) {
|
||||
double rem = 0.0;
|
||||
int rem_time = 0;
|
||||
if (this->dist_buff_size == this->buffSize) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "homeform.h"
|
||||
#include "mainwindow.h"
|
||||
#include "qfit.h"
|
||||
#include "share_send_recv/applicationui.hpp"
|
||||
#include "virtualtreadmill.h"
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
@@ -565,8 +566,13 @@ int main(int argc, char *argv[]) {
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
ApplicationUI appui(homeform::getWritableAppDir());
|
||||
appui.addContextProperty(engine.rootContext());
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
engine.rootContext()->setContextProperty("OS_VERSION", QVariant("Android"));
|
||||
QObject::connect(app.get(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), &appui,
|
||||
SLOT(onApplicationStateChanged(Qt::ApplicationState)));
|
||||
#elif defined(Q_OS_IOS)
|
||||
engine.rootContext()->setContextProperty("OS_VERSION", QVariant("iOS"));
|
||||
#else
|
||||
|
||||
194
src/main.qml
194
src/main.qml
@@ -37,6 +37,150 @@ ApplicationWindow {
|
||||
signal restart()
|
||||
signal volumeUp()
|
||||
signal volumeDown()
|
||||
property string lastSettingsFileName: ""
|
||||
function readTextFile(fileUrl, handle) {
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.open("GET", fileUrl); // set Method and File
|
||||
xhr.onreadystatechange = function () {
|
||||
if(xhr.readyState === XMLHttpRequest.DONE){ // if request_status == DONE
|
||||
var response = xhr.responseText;
|
||||
|
||||
console.log(response);
|
||||
if (handle)
|
||||
handle(fileUrl, response);
|
||||
// Your Code
|
||||
}
|
||||
}
|
||||
xhr.send(); // begin the request
|
||||
}
|
||||
|
||||
function notify_load_on_intent(url) {
|
||||
if (!url.startsWith('file://'))
|
||||
url = 'file://' + url;
|
||||
if(Qt.platform.os === "android") {
|
||||
if(!appui.checkPermission()) {
|
||||
popupLoadSaveFile.labelText = qsTr("Loading settings needs permission for external storage\n%1").arg(url)
|
||||
popupLoadSaveFile.open()
|
||||
return
|
||||
}
|
||||
}
|
||||
console.log("notify_load_on_intent: " + url);
|
||||
if (url.endsWith(".qzs"))
|
||||
loadSettings(url);
|
||||
else
|
||||
trainprogram_open_clicked(url);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: rootItem
|
||||
onTrainProgramLoaded: {
|
||||
let qmlString = name;
|
||||
let idx = qmlString.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
lastSettingsFileName = qmlString.substring(idx + 1);
|
||||
if (lines) {
|
||||
popupLoadSaveFile.labelText = qsTr("Program has been loaded correctly (%1 lines).\nFile name was %2").arg(lines).arg(lastSettingsFileName);
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
else {
|
||||
popupLoadSaveFile.labelText = qsTr("Cannot save settings to file\n%1").arg(lastSettingsFileName);
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shareUtils
|
||||
onFileUrlReceived: {
|
||||
console.log("Loading settings from "+url)
|
||||
notify_load_on_intent(url);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shareUtils
|
||||
onFileReceivedAndSaved: {
|
||||
console.log("Loading settings from my saved "+url)
|
||||
notify_load_on_intent(url)
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: rootItem
|
||||
onSettingsSaved: {
|
||||
let qmlString = settingsFileUri;
|
||||
let idx = qmlString.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
lastSettingsFileName = qmlString.substring(idx + 1);
|
||||
if (settingsN) {
|
||||
shareUtils.sendFile(appui.filePathDocumentsLocation(qmlString), "Send Settings file", "text/plain", Constants.SHARE_REQUEST_CODE, 0)
|
||||
}
|
||||
else {
|
||||
popupLoadSaveFile.labelText = qsTr("Cannot save settings to file\n%1").arg(lastSettingsFileName);
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: rootItem
|
||||
onSettingsLoaded: {
|
||||
let qmlString = settingsFileUri;
|
||||
let idx = qmlString.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
lastSettingsFileName = qmlString.substring(idx + 1);
|
||||
if (settingsN) {
|
||||
popupLoadSaveFile.labelText = qsTr("Settings has been loaded correctly (%1 keys). Restart the app!\nFile name was %2").arg(settingsN).arg(lastSettingsFileName);
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
else {
|
||||
popupLoadSaveFile.labelText = qsTr("Cannot save settings to file\n%1").arg(lastSettingsFileName);
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: shareUtils
|
||||
onShareEditDone: {
|
||||
console.log("ShareEditDone " + requestCode);
|
||||
if (requestCode == Constants.SHARE_REQUEST_CODE) {
|
||||
popupLoadSaveFile.labelText = qsTr("Share settings file\n" + lastSettingsFileName + " OK")
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: shareUtils
|
||||
onShareFinished: {
|
||||
console.log("ShareFinished " + requestCode);
|
||||
if (requestCode == Constants.SHARE_REQUEST_CODE) {
|
||||
popupLoadSaveFile.labelText = qsTr("Share settings file\n" + lastSettingsFileName + " cancelled")
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: shareUtils
|
||||
onShareNoAppAvailable: {
|
||||
console.log("ShareAppNo " + requestCode);
|
||||
if (requestCode == Constants.SHARE_REQUEST_CODE) {
|
||||
popupLoadSaveFile.labelText = qsTr("No app available to share settings file\n" + lastSettingsFileName)
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: shareUtils
|
||||
onShareError: {
|
||||
console.log("ShareError " + requestCode);
|
||||
if (requestCode == Constants.SHARE_REQUEST_CODE) {
|
||||
popupLoadSaveFile.labelText = qsTr("Share settings file\n" + lastSettingsFileName + "\nerror: " + message)
|
||||
popupLoadSaveFile.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signal keyMediaPrevious()
|
||||
signal keyMediaNext()
|
||||
signal floatingOpen()
|
||||
@@ -252,37 +396,9 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupLoadSettings
|
||||
parent: Overlay.overlay
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
width: 380
|
||||
height: 60
|
||||
modal: true
|
||||
focus: true
|
||||
palette.text: "white"
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
enter: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 }
|
||||
}
|
||||
exit: Transition
|
||||
{
|
||||
NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 }
|
||||
}
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Settings has been loaded correctly. Restart the app!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popupSaveFile
|
||||
id: popupLoadSaveFile
|
||||
parent: Overlay.overlay
|
||||
property alias labelText: popupLabel.text
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
@@ -303,8 +419,9 @@ ApplicationWindow {
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Label {
|
||||
id: popupLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Saved! Check your private folder (Android)<br>or Files App (iOS)")
|
||||
text: qsTr("Send File OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,13 +620,15 @@ ApplicationWindow {
|
||||
icon.source: "icons/icons/tray-arrow-up.png"
|
||||
onClicked: {
|
||||
stackView.push("SettingsList.qml")
|
||||
stackView.currentItem.loadSettings.connect(loadSettings)
|
||||
stackView.currentItem.loadSettings.connect(function(url) {
|
||||
console.log("Simulating intent " + url);
|
||||
appui.simulateIntentReceived(url);
|
||||
stackView.pop();
|
||||
if (stackView.depth > 1) {
|
||||
stackView.pop()
|
||||
}
|
||||
popupLoadSettings.open();
|
||||
// popupLoadSaveFile.labelText = qsTr("Settings has been loaded correctly. Restart the app!")
|
||||
// popupLoadSaveFile.open();
|
||||
});
|
||||
drawer.close()
|
||||
}
|
||||
@@ -522,7 +641,6 @@ ApplicationWindow {
|
||||
icon.source: "icons/icons/tray-arrow-down.png"
|
||||
onClicked: {
|
||||
saveSettings("settings");
|
||||
popupSaveFile.open()
|
||||
}
|
||||
anchors.right: toolButtonAutoResistance.left/*toolClassifica.left*/
|
||||
visible: false
|
||||
@@ -679,8 +797,8 @@ ApplicationWindow {
|
||||
stackView.currentItem.trainprogram_open_other_folder.connect(trainprogram_open_other_folder)
|
||||
stackView.currentItem.trainprogram_preview.connect(trainprogram_preview)
|
||||
stackView.currentItem.trainprogram_open_clicked.connect(function(url) {
|
||||
appui.simulateIntentReceived(url);
|
||||
stackView.pop();
|
||||
popup.open();
|
||||
});
|
||||
drawer.close()
|
||||
}
|
||||
@@ -701,7 +819,8 @@ ApplicationWindow {
|
||||
onClicked: {
|
||||
gpx_save_clicked()
|
||||
drawer.close()
|
||||
popupSaveFile.open()
|
||||
popupLoadSaveFile.labelText = qsTr("Saved! Check your private folder (Android)<br>or Files App (iOS)")
|
||||
popupLoadSaveFile.open()
|
||||
}
|
||||
}
|
||||
ItemDelegate {
|
||||
@@ -711,7 +830,8 @@ ApplicationWindow {
|
||||
onClicked: {
|
||||
fit_save_clicked()
|
||||
drawer.close()
|
||||
popupSaveFile.open()
|
||||
popupLoadSaveFile.labelText = qsTr("Saved! Check your private folder (Android)<br>or Files App (iOS)")
|
||||
popupLoadSaveFile.open()
|
||||
}
|
||||
}
|
||||
ItemDelegate {
|
||||
|
||||
@@ -30,6 +30,12 @@ CONFIG += qmltypes
|
||||
#win32: CONFIG += webengine
|
||||
#unix:!android: CONFIG += webengine
|
||||
|
||||
android: {
|
||||
include(../android_openssl/openssl.pri)
|
||||
SOURCES += share_send_recv/android/androidshareutils.cpp
|
||||
HEADERS += share_send_recv/android/androidshareutils.hpp
|
||||
}
|
||||
|
||||
QML_IMPORT_NAME = org.cagnulein.qdomyoszwift
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
@@ -798,11 +804,16 @@ ios {
|
||||
fit-sdk/FitMesg.mm \
|
||||
fit-sdk/FitMesgDefinition.mm \
|
||||
ios/M3iNS.mm \
|
||||
ios/share_send_recv/iosshareutils.mm \
|
||||
ios/share_send_recv/docviewcontroller.mm
|
||||
|
||||
SOURCES += ios/M3iNSQT.cpp
|
||||
|
||||
OBJECTIVE_HEADERS += ios/M3iNS.h
|
||||
|
||||
HEADERS += share_send_recv/ios/iosshareutils.hpp \
|
||||
share_send_recv/ios/docviewcontroller.hpp
|
||||
|
||||
QMAKE_INFO_PLIST = ios/Info.plist
|
||||
QMAKE_ASSET_CATALOGS = $$PWD/ios/Images.xcassets
|
||||
QMAKE_ASSET_CATALOGS_APP_ICON = "AppIcon"
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<file>Credits.qml</file>
|
||||
<file>WebEngineTest.qml</file>
|
||||
<file>profiles.qml</file>
|
||||
<file>Constants.qml</file>
|
||||
<file>SwagBagView.qml</file>
|
||||
<file>SimpleButton.qml</file>
|
||||
<file>SwagBagItem.qml</file>
|
||||
|
||||
627
src/share_send_recv/android/androidshareutils.cpp
Executable file
627
src/share_send_recv/android/androidshareutils.cpp
Executable file
@@ -0,0 +1,627 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#include "androidshareutils.hpp"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
#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/cagnulen/qdomyoszwift/share_send_recv/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/cagnulen/qdomyoszwift/share_send_recv/QShareUtils", "checkMimeTypeEdit", "(Ljava/lang/String;)Z",
|
||||
jsMime.object<jstring>());
|
||||
qDebug() << "Edit VERIFIED: " << mimeType << " - " << verified;
|
||||
return verified;
|
||||
}
|
||||
|
||||
void AndroidShareUtils::share(const QString &text, const QUrl &url) {
|
||||
QAndroidJniObject jsText = QAndroidJniObject::fromString(text);
|
||||
QAndroidJniObject jsUrl = QAndroidJniObject::fromString(url.toString());
|
||||
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/cagnulen/qdomyoszwift/share_send_recv/QShareUtils",
|
||||
"share", "(Ljava/lang/String;Ljava/lang/String;)Z",
|
||||
jsText.object<jstring>(), jsUrl.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, const bool &altImpl) {
|
||||
mIsEditMode = false;
|
||||
|
||||
if (!altImpl) {
|
||||
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
|
||||
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
|
||||
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
|
||||
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>(
|
||||
"org/cagnulen/qdomyoszwift/share_send_recv/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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// THE FILE PATH
|
||||
// to get a valid Path we must prefix file://
|
||||
// attention file must be inside Users Documents folder !
|
||||
// trying to send a file from APP DATA will fail
|
||||
QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://" + filePath);
|
||||
if (!jniPath.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniPath not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid"));
|
||||
return;
|
||||
}
|
||||
// next step: convert filePath Java String into Java Uri
|
||||
QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod(
|
||||
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object<jstring>());
|
||||
if (!jniUri.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniUri not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nURI not valid"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE INTENT ACTION
|
||||
// create a Java String for the ACTION
|
||||
QAndroidJniObject jniAction =
|
||||
QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "ACTION_SEND");
|
||||
if (!jniAction.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniParam not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
// then create the Intent Object for this Action
|
||||
QAndroidJniObject jniIntent("android/content/Intent", "(Ljava/lang/String;)V", jniAction.object<jstring>());
|
||||
if (!jniIntent.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniIntent not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE MIME TYPE
|
||||
if (mimeType.isEmpty()) {
|
||||
qWarning() << "mime type is empty";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty"));
|
||||
return;
|
||||
}
|
||||
// create a Java String for the File Type (Mime Type)
|
||||
QAndroidJniObject jniMimeType = QAndroidJniObject::fromString(mimeType);
|
||||
if (!jniMimeType.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniMimeType not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid"));
|
||||
return;
|
||||
}
|
||||
// set Type (MimeType)
|
||||
QAndroidJniObject jniType = jniIntent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;",
|
||||
jniMimeType.object<jstring>());
|
||||
if (!jniType.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniType not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE EXTRA STREAM
|
||||
// create a Java String for the EXTRA
|
||||
QAndroidJniObject jniExtra =
|
||||
QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "EXTRA_STREAM");
|
||||
if (!jniExtra.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniExtra not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
// put Extra (EXTRA_STREAM and URI)
|
||||
QAndroidJniObject jniExtraStreamUri =
|
||||
jniIntent.callObjectMethod("putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;",
|
||||
jniExtra.object<jstring>(), jniUri.object<jobject>());
|
||||
// QAndroidJniObject jniExtraStreamUri = jniIntent.callObjectMethod("putExtra",
|
||||
// "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", jniExtra.object<jstring>(),
|
||||
// jniExtra.object<jstring>());
|
||||
if (!jniExtraStreamUri.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniExtraStreamUri not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
QAndroidJniObject activity = QtAndroid::androidActivity();
|
||||
QAndroidJniObject packageManager =
|
||||
activity.callObjectMethod("getPackageManager", "()Landroid/content/pm/PackageManager;");
|
||||
QAndroidJniObject componentName = jniIntent.callObjectMethod(
|
||||
"resolveActivity", "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;",
|
||||
packageManager.object());
|
||||
if (!componentName.isValid()) {
|
||||
qWarning() << "Unable to resolve activity";
|
||||
emit shareNoAppAvailable(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Starting activity" << requestId;
|
||||
if (requestId <= 0) {
|
||||
// we dont need a result if there's no requestId
|
||||
QtAndroid::startActivity(jniIntent, requestId);
|
||||
} else {
|
||||
// we have the JNI Object, know the requestId
|
||||
// and want the Result back into 'this' handleActivityResult(...)
|
||||
// attention: to test JNI with QAndroidActivityResultReceiver you must comment or rename
|
||||
// onActivityResult() method in QShareActivity.java - otherwise you'll get wrong request or result codes
|
||||
QtAndroid::startActivity(jniIntent, requestId, this);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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, const bool &altImpl) {
|
||||
mIsEditMode = false;
|
||||
|
||||
if (!altImpl) {
|
||||
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
|
||||
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
|
||||
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
|
||||
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>(
|
||||
"org/cagnulen/qdomyoszwift/share_send_recv/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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// THE FILE PATH
|
||||
// to get a valid Path we must prefix file://
|
||||
// attention file must be inside Users Documents folder !
|
||||
// trying to view or edit a file from APP DATA will fail
|
||||
QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://" + filePath);
|
||||
if (!jniPath.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniPath not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid"));
|
||||
return;
|
||||
}
|
||||
// next step: convert filePath Java String into Java Uri
|
||||
QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod(
|
||||
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object<jstring>());
|
||||
if (!jniUri.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniUri not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nURI not valid"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE INTENT ACTION
|
||||
// create a Java String for the ACTION
|
||||
QAndroidJniObject jniParam =
|
||||
QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "ACTION_VIEW");
|
||||
if (!jniParam.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniParam not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
// then create the Intent Object for this Action
|
||||
QAndroidJniObject jniIntent("android/content/Intent", "(Ljava/lang/String;)V", jniParam.object<jstring>());
|
||||
if (!jniIntent.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniIntent not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE FILE TYPE
|
||||
if (mimeType.isEmpty()) {
|
||||
qWarning() << "mime type is empty";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty"));
|
||||
return;
|
||||
}
|
||||
// create a Java String for the File Type (Mime Type)
|
||||
QAndroidJniObject jniType = QAndroidJniObject::fromString(mimeType);
|
||||
if (!jniType.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniType not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid"));
|
||||
return;
|
||||
}
|
||||
// set Data (the URI) and Type (MimeType)
|
||||
QAndroidJniObject jniResult =
|
||||
jniIntent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;",
|
||||
jniUri.object<jobject>(), jniType.object<jstring>());
|
||||
if (!jniResult.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniResult not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
QAndroidJniObject activity = QtAndroid::androidActivity();
|
||||
QAndroidJniObject packageManager =
|
||||
activity.callObjectMethod("getPackageManager", "()Landroid/content/pm/PackageManager;");
|
||||
QAndroidJniObject componentName = jniIntent.callObjectMethod(
|
||||
"resolveActivity", "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;",
|
||||
packageManager.object());
|
||||
if (!componentName.isValid()) {
|
||||
qWarning() << "Unable to resolve activity";
|
||||
emit shareNoAppAvailable(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestId <= 0) {
|
||||
// we dont need a result if there's no requestId
|
||||
QtAndroid::startActivity(jniIntent, requestId);
|
||||
} else {
|
||||
// we have the JNI Object, know the requestId
|
||||
// and want the Result back into 'this' handleActivityResult(...)
|
||||
// attention: to test JNI with QAndroidActivityResultReceiver you must comment or rename
|
||||
// onActivityResult() method in QShareActivity.java - otherwise you'll get wrong request or result codes
|
||||
QtAndroid::startActivity(jniIntent, requestId, this);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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, const bool &altImpl) {
|
||||
mIsEditMode = true;
|
||||
mCurrentFilePath = filePath;
|
||||
QFileInfo fileInfo = QFileInfo(mCurrentFilePath);
|
||||
mLastModified = fileInfo.lastModified().toSecsSinceEpoch();
|
||||
qDebug() << "LAST MODIFIED: " << mLastModified;
|
||||
|
||||
if (!altImpl) {
|
||||
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
|
||||
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
|
||||
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
|
||||
|
||||
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>(
|
||||
"org/cagnulen/qdomyoszwift/share_send_recv/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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// THE FILE PATH
|
||||
// to get a valid Path we must prefix file://
|
||||
// attention file must be inside Users Documents folder !
|
||||
// trying to view or edit a file from APP DATA will fail
|
||||
QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://" + filePath);
|
||||
if (!jniPath.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniPath not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid"));
|
||||
return;
|
||||
}
|
||||
// next step: convert filePath Java String into Java Uri
|
||||
QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod(
|
||||
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object<jstring>());
|
||||
if (!jniUri.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniUri not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nURI not valid"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE INTENT ACTION
|
||||
// create a Java String for the ACTION
|
||||
QAndroidJniObject jniParam =
|
||||
QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "ACTION_EDIT");
|
||||
if (!jniParam.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniParam not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
// then create the Intent Object for this Action
|
||||
QAndroidJniObject jniIntent("android/content/Intent", "(Ljava/lang/String;)V", jniParam.object<jstring>());
|
||||
if (!jniIntent.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniIntent not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
// THE FILE TYPE
|
||||
if (mimeType.isEmpty()) {
|
||||
qWarning() << "mime type is empty";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty"));
|
||||
return;
|
||||
}
|
||||
// create a Java String for the File Type (Mime Type)
|
||||
QAndroidJniObject jniType = QAndroidJniObject::fromString(mimeType);
|
||||
if (!jniType.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniType not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid"));
|
||||
return;
|
||||
}
|
||||
// set Data (the URI) and Type (MimeType)
|
||||
QAndroidJniObject jniResult =
|
||||
jniIntent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;",
|
||||
jniUri.object<jobject>(), jniType.object<jstring>());
|
||||
if (!jniResult.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniResult not valid.";
|
||||
emit shareError(requestId, tr("Share: an Error occured"));
|
||||
return;
|
||||
}
|
||||
|
||||
QAndroidJniObject activity = QtAndroid::androidActivity();
|
||||
QAndroidJniObject packageManager =
|
||||
activity.callObjectMethod("getPackageManager", "()Landroid/content/pm/PackageManager;");
|
||||
QAndroidJniObject componentName = jniIntent.callObjectMethod(
|
||||
"resolveActivity", "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;",
|
||||
packageManager.object());
|
||||
if (!componentName.isValid()) {
|
||||
qWarning() << "Unable to resolve activity";
|
||||
emit shareNoAppAvailable(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// now all is ready to start the Activity:
|
||||
if (requestId <= 0) {
|
||||
// we dont need a result if there's no requestId
|
||||
QtAndroid::startActivity(jniIntent, requestId);
|
||||
} else {
|
||||
// we have the JNI Object, know the requestId
|
||||
// and want the Result back into 'this' handleActivityResult(...)
|
||||
// attention: to test JNI with QAndroidActivityResultReceiver you must comment or rename
|
||||
// onActivityResult() method in QShareActivity.java - otherwise you'll get wrong request or result codes
|
||||
QtAndroid::startActivity(jniIntent, requestId, this);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
qDebug() << "result code: " << resultCode << " from request: " << requestCode;
|
||||
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::simulateIntentReceived(const QUrl &url, const QString &workingDirPath) {
|
||||
QAndroidJniObject activity = QtAndroid::androidActivity();
|
||||
if (activity.isValid()) {
|
||||
// create a Java String for the Working Dir Path
|
||||
QAndroidJniObject jniWorkingDir = QAndroidJniObject::fromString(workingDirPath);
|
||||
QAndroidJniObject jniUrl = QAndroidJniObject::fromString(url.toString());
|
||||
if (!jniWorkingDir.isValid() || !jniUrl.isValid()) {
|
||||
qWarning() << "QAndroidJniObject jniWorkingDir not valid.";
|
||||
emit shareError(0, tr("Share: an Error occured\nWorkingDir not valid"));
|
||||
return;
|
||||
}
|
||||
activity.callMethod<void>("processIncomingUri", "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
jniUrl.object<jstring>(), jniWorkingDir.object<jstring>());
|
||||
qDebug() << "processIncomingUri: " << url.toString() << 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" << myUrl << " exists for Qt";
|
||||
QFile file(myUrl);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qDebug() << "Ops Qt cannot read from given file";
|
||||
return false;
|
||||
} else
|
||||
file.close();
|
||||
return true;
|
||||
} else {
|
||||
qDebug() << "Uuups: FILE" << myUrl << " 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_cagnulen_qdomyoszwift_share_1send_1recv_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_cagnulen_qdomyoszwift_share_1send_1recv_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_cagnulen_qdomyoszwift_share_1send_1recv_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_cagnulen_qdomyoszwift_share_1send_1recv_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
|
||||
49
src/share_send_recv/android/androidshareutils.hpp
Executable file
49
src/share_send_recv/android/androidshareutils.hpp
Executable file
@@ -0,0 +1,49 @@
|
||||
// (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 <QAndroidActivityResultReceiver>
|
||||
#include <QtAndroid>
|
||||
|
||||
#include "../shareutils.hpp"
|
||||
|
||||
class AndroidShareUtils : public PlatformShareUtils, public QAndroidActivityResultReceiver {
|
||||
public:
|
||||
AndroidShareUtils(QObject *parent = nullptr);
|
||||
bool checkMimeTypeView(const QString &mimeType) override;
|
||||
bool checkMimeTypeEdit(const QString &mimeType) override;
|
||||
void share(const QString &text, const QUrl &url) override;
|
||||
void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) override;
|
||||
void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) override;
|
||||
void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) override;
|
||||
|
||||
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;
|
||||
void onActivityResult(int requestCode, int resultCode);
|
||||
|
||||
void checkPendingIntents(const QString workingDirPath) override;
|
||||
void simulateIntentReceived(const QUrl &url, 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
|
||||
107
src/share_send_recv/applicationui.cpp
Normal file
107
src/share_send_recv/applicationui.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#include "applicationui.hpp"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QtQml>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include <QtAndroid>
|
||||
#endif
|
||||
|
||||
ApplicationUI::ApplicationUI(const QString &pth, QObject *parent) : QObject(parent), mShareUtils(new ShareUtils(this)) {
|
||||
// this is a demo application where we deal with an Image and a PDF as example
|
||||
// Image and PDF are delivered as qrc:/ resources at /data_assets
|
||||
// to start the tests as first we must copy these 2 files from assets into APP DATA
|
||||
// so we can simulate HowTo view, edit or send files from inside your APP DATA to other APPs
|
||||
// in a real life app you'll have your own workflows
|
||||
// I made copyAssetsToAPPData() INVOKABLE to be able to reset to origin files
|
||||
mAppDataFilesPath = pth;
|
||||
mAppDataFilesPath =
|
||||
(mAppDataFilesPath.isEmpty() ? QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).value(0)
|
||||
: mAppDataFilesPath)
|
||||
.append(QStringLiteral("/contentresolver"));
|
||||
if (!QDir(mAppDataFilesPath).exists()) {
|
||||
if (QDir("").mkpath(mAppDataFilesPath)) {
|
||||
qDebug() << "Created Documents Location work directory. " << mAppDataFilesPath;
|
||||
} else {
|
||||
qWarning() << "Failed to create Documents Location work directory. " << mAppDataFilesPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplicationUI::addContextProperty(QQmlContext *context) {
|
||||
context->setContextProperty("appui", this);
|
||||
context->setContextProperty("shareUtils", mShareUtils);
|
||||
}
|
||||
|
||||
QString ApplicationUI::filePathDocumentsLocation(const QString &sourceFilePath) {
|
||||
QFileInfo fi(sourceFilePath);
|
||||
QString destinationFilePath = sourceFilePath;
|
||||
if (fi.exists() && fi.isFile()) {
|
||||
destinationFilePath = mAppDataFilesPath + QStringLiteral("/") + fi.fileName();
|
||||
qDebug() << "Destination is" << destinationFilePath;
|
||||
if (QFile::exists(destinationFilePath)) {
|
||||
bool removed = QFile::remove(destinationFilePath);
|
||||
if (!removed) {
|
||||
qWarning() << "Failed to remove " << destinationFilePath;
|
||||
return destinationFilePath;
|
||||
}
|
||||
}
|
||||
bool copied = QFile::copy(sourceFilePath, destinationFilePath);
|
||||
if (!copied) {
|
||||
qWarning() << "Failed to copy " << sourceFilePath << " to " << destinationFilePath;
|
||||
//#if defined(Q_OS_ANDROID)
|
||||
// emit noDocumentsWorkLocation();
|
||||
//#endif
|
||||
}
|
||||
}
|
||||
return destinationFilePath;
|
||||
}
|
||||
|
||||
void ApplicationUI::simulateIntentReceived(const QUrl &sourceFilePath) {
|
||||
mShareUtils->simulateIntentReceived(sourceFilePath, mAppDataFilesPath);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
void ApplicationUI::onApplicationStateChanged(Qt::ApplicationState applicationState) {
|
||||
qDebug() << "S T A T E changed into: " << applicationState;
|
||||
if (applicationState == Qt::ApplicationState::ApplicationSuspended) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
if (applicationState == Qt::ApplicationState::ApplicationActive) {
|
||||
// if App was launched from VIEW or SEND Intent
|
||||
// there's a race collision: the event will be lost,
|
||||
// because App and UI wasn't completely initialized
|
||||
// workaround: QShareActivity remembers that an Intent is pending
|
||||
if (!mPendingIntentsChecked) {
|
||||
mPendingIntentsChecked = true;
|
||||
mShareUtils->checkPendingIntents(mAppDataFilesPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we don't need permissions if we only share files to other apps using FileProvider
|
||||
// but we need permissions if other apps share their files with out app and we must access those files
|
||||
bool ApplicationUI::checkPermission() {
|
||||
QtAndroid::PermissionResult r = QtAndroid::checkPermission("android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
if (r == QtAndroid::PermissionResult::Denied) {
|
||||
QtAndroid::requestPermissionsSync(QStringList() << "android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
r = QtAndroid::checkPermission("android.permission.WRITE_EXTERNAL_STORAGE");
|
||||
if (r == QtAndroid::PermissionResult::Denied) {
|
||||
qDebug() << "Permission denied";
|
||||
emit noDocumentsWorkLocation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "YEP: Permission OK";
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
46
src/share_send_recv/applicationui.hpp
Normal file
46
src/share_send_recv/applicationui.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#ifndef APPLICATIONUI_HPP
|
||||
#define APPLICATIONUI_HPP
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "shareutils.hpp"
|
||||
#include <QtQml>
|
||||
|
||||
class ApplicationUI : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ApplicationUI(const QString &pth = QStringLiteral(""), QObject *parent = 0);
|
||||
|
||||
void addContextProperty(QQmlContext *context);
|
||||
|
||||
Q_INVOKABLE
|
||||
void simulateIntentReceived(const QUrl &sourceFilePath);
|
||||
|
||||
Q_INVOKABLE
|
||||
QString filePathDocumentsLocation(const QString &sourceFilePath);
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
Q_INVOKABLE
|
||||
bool checkPermission();
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void noDocumentsWorkLocation();
|
||||
|
||||
public slots:
|
||||
#if defined(Q_OS_ANDROID)
|
||||
void onApplicationStateChanged(Qt::ApplicationState applicationState);
|
||||
#endif
|
||||
|
||||
private:
|
||||
ShareUtils *mShareUtils;
|
||||
QString mAppDataFilesPath;
|
||||
bool mPendingIntentsChecked;
|
||||
};
|
||||
|
||||
#endif // APPLICATIONUI_HPP
|
||||
21
src/share_send_recv/ios/docviewcontroller.hpp
Normal file
21
src/share_send_recv/ios/docviewcontroller.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#ifndef DOCVIEWCONTROLLER_HPP
|
||||
#define DOCVIEWCONTROLLER_HPP
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "iosshareutils.hpp"
|
||||
|
||||
@interface DocViewController : UIViewController <UIDocumentInteractionControllerDelegate>
|
||||
|
||||
@property int requestId;
|
||||
|
||||
@property IosShareUtils *mIosShareUtils;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
#endif // DOCVIEWCONTROLLER_HPP
|
||||
31
src/share_send_recv/ios/iosshareutils.hpp
Executable file
31
src/share_send_recv/ios/iosshareutils.hpp
Executable file
@@ -0,0 +1,31 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#ifndef __IOSSHAREUTILS_H__
|
||||
#define __IOSSHAREUTILS_H__
|
||||
|
||||
#include "../shareutils.hpp"
|
||||
|
||||
class IosShareUtils : public PlatformShareUtils
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IosShareUtils(QObject *parent = 0);
|
||||
bool checkMimeTypeView(const QString &mimeType);
|
||||
bool checkMimeTypeEdit(const QString &mimeType);
|
||||
void share(const QString &text, const QUrl &url);
|
||||
void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl);
|
||||
void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl);
|
||||
void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl);
|
||||
|
||||
void handleDocumentPreviewDone(const int &requestId);
|
||||
|
||||
public slots:
|
||||
void handleFileUrlReceived(const QUrl &url);
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
89
src/share_send_recv/shareutils.cpp
Executable file
89
src/share_send_recv/shareutils.cpp
Executable file
@@ -0,0 +1,89 @@
|
||||
// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner
|
||||
// my blog about Qt for mobile: http://j.mp/qt-x
|
||||
// see also /COPYRIGHT and /LICENSE
|
||||
|
||||
#include "shareutils.hpp"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "ios/iosshareutils.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "android/androidshareutils.hpp"
|
||||
#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::share(const QString &text, const QUrl &url) { mPlatformShareUtils->share(text, url); }
|
||||
|
||||
void ShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) {
|
||||
mPlatformShareUtils->sendFile(filePath, title, mimeType, requestId, altImpl);
|
||||
}
|
||||
|
||||
void ShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) {
|
||||
mPlatformShareUtils->viewFile(filePath, title, mimeType, requestId, altImpl);
|
||||
}
|
||||
|
||||
void ShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) {
|
||||
mPlatformShareUtils->editFile(filePath, title, mimeType, requestId, altImpl);
|
||||
}
|
||||
|
||||
void ShareUtils::checkPendingIntents(const QString workingDirPath) {
|
||||
mPlatformShareUtils->checkPendingIntents(workingDirPath);
|
||||
}
|
||||
|
||||
void ShareUtils::simulateIntentReceived(const QUrl &url, const QString &workingDirPath) {
|
||||
mPlatformShareUtils->simulateIntentReceived(url, 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); }
|
||||
112
src/share_send_recv/shareutils.hpp
Executable file
112
src/share_send_recv/shareutils.hpp
Executable file
@@ -0,0 +1,112 @@
|
||||
// (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 <QUrl>
|
||||
|
||||
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) : QObject(parent) {}
|
||||
virtual ~PlatformShareUtils() {}
|
||||
virtual bool checkMimeTypeView(const QString &mimeType) {
|
||||
qDebug() << "check view for " << mimeType;
|
||||
return true;
|
||||
}
|
||||
virtual bool checkMimeTypeEdit(const QString &mimeType) {
|
||||
qDebug() << "check edit for " << mimeType;
|
||||
return true;
|
||||
}
|
||||
virtual void share(const QString &text, const QUrl &url) { qDebug() << text << url; }
|
||||
virtual void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) {
|
||||
qDebug() << filePath << " - " << title << "requestId " << requestId << " - " << mimeType << "altImpl? "
|
||||
<< altImpl;
|
||||
}
|
||||
virtual void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) {
|
||||
qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? "
|
||||
<< altImpl;
|
||||
}
|
||||
virtual void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId,
|
||||
const bool &altImpl) {
|
||||
qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? "
|
||||
<< altImpl;
|
||||
}
|
||||
|
||||
virtual void checkPendingIntents(const QString workingDirPath) {
|
||||
qDebug() << "checkPendingIntents " << workingDirPath;
|
||||
}
|
||||
virtual void simulateIntentReceived(const QUrl &url, const QString &workingDirPath) {
|
||||
Q_UNUSED(workingDirPath);
|
||||
QString rv = url.toString();
|
||||
if (rv.startsWith("file://")) {
|
||||
rv = rv.right(rv.length() - 7);
|
||||
emit fileUrlReceived(rv);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 share(const QString &text, const QUrl &url);
|
||||
Q_INVOKABLE void sendFile(const QString &filePath, const QString &title, const QString &mimeType,
|
||||
const int &requestId, const bool &altImpl);
|
||||
Q_INVOKABLE void viewFile(const QString &filePath, const QString &title, const QString &mimeType,
|
||||
const int &requestId, const bool &altImpl);
|
||||
Q_INVOKABLE void editFile(const QString &filePath, const QString &title, const QString &mimeType,
|
||||
const int &requestId, const bool &altImpl);
|
||||
Q_INVOKABLE void checkPendingIntents(const QString workingDirPath);
|
||||
Q_INVOKABLE void simulateIntentReceived(const QUrl &url, const QString &workingDirPath);
|
||||
|
||||
private:
|
||||
PlatformShareUtils *mPlatformShareUtils;
|
||||
};
|
||||
|
||||
#endif // SHAREUTILS_H
|
||||
@@ -25,6 +25,14 @@ function get_template_name() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function get_prefix_name() {
|
||||
let endidx = location.pathname.lastIndexOf('/');
|
||||
if (endidx>=0)
|
||||
return location.pathname.substring(0, endidx);
|
||||
else
|
||||
return location.pathname;
|
||||
}
|
||||
|
||||
function get_url_without_file() {
|
||||
/*let idx = location.pathname.lastIndexOf('/');
|
||||
let end = location.pathname;
|
||||
|
||||
@@ -104,7 +104,7 @@ function main_ws_queue_process(msg) {
|
||||
|
||||
|
||||
function main_ws_connect() {
|
||||
let socket = new WebSocket((location.protocol == 'https:'?'wss://' : 'ws://') + host_url + '/' + get_template_name() + '-ws');
|
||||
let socket = new WebSocket((location.protocol == 'https:'?'wss://' : 'ws://') + host_url + get_prefix_name() + '/' + get_template_name() + '-ws');
|
||||
socket.onopen = function (event) {
|
||||
console.log('Upgrade HTTP connection OK');
|
||||
main_ws = socket;
|
||||
|
||||
6
template-examples/youtube-viewer/peertube_grabber.js
Normal file
6
template-examples/youtube-viewer/peertube_grabber.js
Normal file
@@ -0,0 +1,6 @@
|
||||
if (typeof PeertubeGrabber === 'function')
|
||||
on_grabber_load('peertube', new PeertubeGrabber('peertube.co.uk', 'peertube', urlParams));
|
||||
else
|
||||
dyn_module_load('./peertubecommon_grabber.js', function() {
|
||||
on_grabber_load('peertube', new PeertubeGrabber('peertube.co.uk', 'peertube', urlParams));
|
||||
});
|
||||
68
template-examples/youtube-viewer/peertubecommon_grabber.js
Normal file
68
template-examples/youtube-viewer/peertubecommon_grabber.js
Normal file
@@ -0,0 +1,68 @@
|
||||
class PeertubeGrabber {
|
||||
constructor(host, name, up) {
|
||||
this.on_grab = null;
|
||||
this.configure(up);
|
||||
this.host = host;
|
||||
this.name = name;
|
||||
}
|
||||
configure(up) {
|
||||
|
||||
}
|
||||
escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
extract_id(el) {
|
||||
let match;
|
||||
if ((match = new RegExp(this.escapeRegExp(this.host)+'/w/([a-zA-Z0-9_\\-]+)', 'i').exec(el)))
|
||||
return match[1];
|
||||
else
|
||||
return el;
|
||||
}
|
||||
get_settings_form(){
|
||||
return null;
|
||||
}
|
||||
form2params(up) {
|
||||
this.configure(up);
|
||||
}
|
||||
grab(value) {
|
||||
value = this.extract_id(value);
|
||||
if (value && value.length) {
|
||||
fetcher_fetch('https://' + this.host + '/api/v1/videos/' + value).then(
|
||||
function(response) {
|
||||
return response.json();
|
||||
}
|
||||
). then( function(json_obj) {
|
||||
let pls;
|
||||
if (json_obj && (pls = json_obj.streamingPlaylists) && pls.length && (pls = pls[0]) && pls.playlistUrl) {
|
||||
let obj = {
|
||||
img: 'https://' + this.host + json_obj.thumbnailPath,
|
||||
uid: value,
|
||||
link: pls.playlistUrl,
|
||||
dur: json_obj.duration,
|
||||
title: json_obj.name
|
||||
};
|
||||
if (this.on_grab)
|
||||
this.on_grab([obj], value, this.name);
|
||||
}
|
||||
else if (this.on_grab)
|
||||
this.on_grab([], 'Unexpected object ' + JSON.stringify(json_obj), this.name);
|
||||
|
||||
}.bind(this)).catch(
|
||||
function(error) {
|
||||
setTimeout(function() {
|
||||
console.error('Error is ' + error);
|
||||
if (this.on_grab)
|
||||
this.on_grab([], '' + error, this.name);
|
||||
}.bind(this), 0);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
else {
|
||||
setTimeout(function() {
|
||||
console.error(value + ' is not a valid playlist or video id');
|
||||
if (this.on_grab)
|
||||
this.on_grab([], value, this.name);
|
||||
}.bind(this), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,13 @@ class PersonalGrabber {
|
||||
this.user = urlParams.has('personal-username')?urlParams.get('personal-username'):(typeof(DEFAULT_PERSONAL_USER) != 'string'? '':DEFAULT_PERSONAL_USER);
|
||||
this.conv = urlParams.has('personal-conv')?urlParams.get('personal-conv'):0;
|
||||
let host = this.url;
|
||||
while (host.lastIndexOf('/') == host.length - 1)
|
||||
host = host.substring(0, host.length -1);
|
||||
let idx;
|
||||
if ((idx = host.lastIndexOf('/')) > 10) {
|
||||
host = host.substring(0, idx);
|
||||
if (host && host.length) {
|
||||
while (host.lastIndexOf('/') == host.length - 1)
|
||||
host = host.substring(0, host.length -1);
|
||||
let idx;
|
||||
if ((idx = host.lastIndexOf('/')) > 10) {
|
||||
host = host.substring(0, idx);
|
||||
}
|
||||
}
|
||||
this.ajax_settings = {
|
||||
type : 'GET',
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" data-grabber="youtube" href="#"><i class="fab fa-youtube"></i> Youtube</a>
|
||||
<a class="dropdown-item" data-grabber="twitch" href="#"><i class="fab fa-twitch"></i> Twitch</a>
|
||||
<a class="dropdown-item" data-grabber="peertube" href="#"><i class="fa-solid fa-play"></i> Peertube</a>
|
||||
<a class="dropdown-item" data-grabber="skrepin" href="#"><i class="fa-solid fa-play"></i> Skrepin</a>
|
||||
<a class="dropdown-item" data-grabber="personal" href="#"><i class="fa fa-border-none"></i> Personal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
let playlist_grabbers_obj = {};
|
||||
let playlist_raw_list = [];
|
||||
let playlist_grabbers = ['youtube', 'personal', 'twitch'];
|
||||
let playlist_grabbers = ['youtube', 'personal', 'twitch', 'peertube', 'skrepin'];
|
||||
let badge_colors = ['primary', 'success', 'secondary', 'danger', 'warning', 'info', 'light', 'dark'];
|
||||
let playlist_load_idx = 0;
|
||||
let playlist_current_id = 0;
|
||||
|
||||
@@ -182,7 +182,7 @@ function base64toBlob(base64Data, contentType) {
|
||||
}
|
||||
|
||||
function fetcher_connect() {
|
||||
let mysocket = new WebSocket((location.protocol == 'https:'?'wss://' : 'ws://') + host_url + '/fetcher');
|
||||
let mysocket = new WebSocket((location.protocol == 'https:'?'wss://' : 'ws://') + host_url + get_prefix_name() + '/fetcher');
|
||||
mysocket.onopen = function (event) {
|
||||
console.log('Fetcher Upgrade HTTP connection OK');
|
||||
fetcher_socket = mysocket;
|
||||
|
||||
6
template-examples/youtube-viewer/skrepin_grabber.js
Normal file
6
template-examples/youtube-viewer/skrepin_grabber.js
Normal file
@@ -0,0 +1,6 @@
|
||||
if (typeof PeertubeGrabber === 'function')
|
||||
on_grabber_load('skrepin', new PeertubeGrabber('tube.skrep.in', 'skrepin', urlParams));
|
||||
else
|
||||
dyn_module_load('./peertubecommon_grabber.js', function() {
|
||||
on_grabber_load('skrepin', new PeertubeGrabber('tube.skrep.in', 'skrepin', urlParams));
|
||||
});
|
||||
@@ -41,7 +41,7 @@ class TwitchGrabber {
|
||||
|
||||
identify_query(pls_name) {
|
||||
let match;
|
||||
if ((match = /(channel|vod)\s*=\s*([^\s*]+)/i.exec(pls_name))) {
|
||||
if ((match = /(channel|vod|vods)\s*=\s*([^\s*]+)/i.exec(pls_name))) {
|
||||
let out = {};
|
||||
out[match[1]] = match[2];
|
||||
return out;
|
||||
@@ -62,14 +62,14 @@ class TwitchGrabber {
|
||||
else if (!this.access || !this.token) {
|
||||
if (new Date().getTime() - this.time_last_refresh > 15000) {
|
||||
let secure_code = generate_rand_string(32);
|
||||
let url = `https://id.twitch.tv/oauth2/authorize?response_type=token+id_token&client_id=${TWITCH_CLIENT_ID}&redirect_uri=${window.location.protocol}//${window.location.host}/${get_template_name()}/twitch-id.htm&scope=viewing_activity_read+openid&state=${secure_code}`;
|
||||
this.time_last_refresh = new Date().getTime();
|
||||
let url = `https://id.twitch.tv/oauth2/authorize?response_type=token+id_token&client_id=${TWITCH_CLIENT_ID}&redirect_uri=${window.location.protocol}//${window.location.host}${get_prefix_name()}/twitch-id.htm&scope=viewing_activity_read+openid&state=${secure_code}`;
|
||||
window.open(url);
|
||||
window.addEventListener('id-window-closed', function(ev) {
|
||||
let event = ev.detail;
|
||||
if (event.error)
|
||||
this.on_fail(event.error);
|
||||
else if (secure_code == event.state) {
|
||||
this.time_last_refresh = new Date().getTime();
|
||||
this.access = event.access;
|
||||
this.token = event.token;
|
||||
this.ajax_settings.headers.Authorization = 'Bearer ' + this.access;
|
||||
@@ -84,6 +84,14 @@ class TwitchGrabber {
|
||||
this.on_grab([], this.ajax_settings.raw_query, 'twitch');
|
||||
}
|
||||
else {
|
||||
if (query_type.vods) {
|
||||
if (!this.ajax_settings.vods) {
|
||||
this.ajax_settings.vods = query_type.vods.split(',');
|
||||
this.ajax_settings.vodsI = 0;
|
||||
this.ajax_settings.videos = [];
|
||||
}
|
||||
query_type.vod = this.ajax_settings.vods[this.ajax_settings.vodsI++];
|
||||
}
|
||||
this.ajax_settings.raw_query = pls_name;
|
||||
this.ajax_settings.query = query_type.channel || query_type.vod;
|
||||
this.ajax_settings.query_type = query_type;
|
||||
@@ -135,7 +143,8 @@ class TwitchGrabber {
|
||||
for (let d of json_obj.data) {
|
||||
d.link = TWITCH_VIDEO_ID_PRE + d.id;
|
||||
d.uid = TWITCH_VIDEO_ID_PRE + d.id;
|
||||
d.img = d.thumbnail_url;
|
||||
d.img = d.thumbnail_url.replace('%{width}','500').replace('%{height}','500');
|
||||
d.uploader = {name: this.ajax_settings.query};
|
||||
let match = /^(?:([0-9]+)d)?(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s)?$/.exec(d.duration);
|
||||
let dur = 0;
|
||||
if (match) {
|
||||
@@ -150,6 +159,20 @@ class TwitchGrabber {
|
||||
}
|
||||
d.dur = dur;
|
||||
}
|
||||
json_obj.data = json_obj.data.reverse();
|
||||
if (this.ajax_settings.vods) {
|
||||
this.ajax_settings.videos = this.ajax_settings.videos.concat(json_obj.data);
|
||||
if (this.ajax_settings.vodsI <this.ajax_settings.vods.length) {
|
||||
this.grab(this.ajax_settings.raw_query);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
json_obj.data = this.ajax_settings.videos;
|
||||
this.ajax_settings.videos.sort(function(a, b) {
|
||||
return a.published_at.localeCompare(b.published_at);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.on_grab) {
|
||||
this.on_grab(json_obj.data, this.ajax_settings.raw_query, 'twitch');
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class TwitchPlayer {
|
||||
$('#player').append($vid);
|
||||
this.on_play_finished = null;
|
||||
this.on_state_changed = null;
|
||||
this.vid = TWITCH_VIDEO_ID_PRE;
|
||||
this.state = VIDEO_STATUS_UNSTARTED;
|
||||
this.embed = this.player = null;
|
||||
this.embed = new Twitch.Embed('twitch-video', options);
|
||||
@@ -51,7 +52,7 @@ class TwitchPlayer {
|
||||
3 (buffering)
|
||||
5 (video cued).*/
|
||||
onPlayerStateChange(event) {
|
||||
if ((event.type == Twitch.Player.OFFLINE || event.type == Twitch.Player.ENDED) && this.on_play_finished) { // ended
|
||||
if (((event.type == Twitch.Player.OFFLINE && !this.vid.startsWith(TWITCH_VIDEO_ID_PRE)) || event.type == Twitch.Player.ENDED) && this.on_play_finished) { // ended
|
||||
this.on_play_finished(this);
|
||||
}
|
||||
if (this.on_state_changed) {
|
||||
@@ -68,12 +69,16 @@ class TwitchPlayer {
|
||||
}
|
||||
|
||||
play_video_id(vid) {
|
||||
this.vid = vid;
|
||||
this.onPlayerStateChange({type: VIDEO_STATUS_PAUSED});
|
||||
if (vid.startsWith(TWITCH_VIDEO_ID_PRE)) {
|
||||
this.player.setChannel(null);
|
||||
this.player.setVideo(vid.substring(TWITCH_VIDEO_ID_PRE.length),5);
|
||||
}
|
||||
else {
|
||||
} else if (/^[0-9]{7,}$/i.exec(vid)) {
|
||||
this.vid = TWITCH_VIDEO_ID_PRE + vid;
|
||||
this.player.setChannel(null);
|
||||
this.player.setVideo(vid, 5);
|
||||
} else {
|
||||
this.player.setVideo(null,5);
|
||||
this.player.setChannel(vid);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class YoutubeGrabber {
|
||||
console.log('Video ' + value +' is ' + JSON.stringify(video.video));
|
||||
//this.on_grab(this.videos);
|
||||
video.video.meta.uid = video.video.videoID;
|
||||
video.video.meta.link = 'https://www.youtube.com/watch?v=' + video.video.uid;
|
||||
video.video.meta.link = 'https://www.youtube.com/watch?v=' + video.video.videoID;
|
||||
if (this.on_grab)
|
||||
this.on_grab([video.video.meta], value, 'youtube');
|
||||
}.bind(this)
|
||||
|
||||
Reference in New Issue
Block a user