Compare commits

...

12 Commits

Author SHA1 Message Date
Roberto Viola
6c9b85c6e5 Update qdomyos-zwift.pri 2023-12-11 15:06:22 +01:00
Roberto Viola
52f16e8410 Merge branch 'master' into pr/916 2023-12-11 15:06:10 +01:00
p3g4asus
8a00025e30 Fix for auto-starting correct twitch video 2022-05-29 12:31:38 +02:00
p3g4asus
c5128eef0a Small fix for twitch player to allow video id without prefix 2022-05-29 12:14:32 +02:00
p3g4asus
a3a67ae14b Several twitch improvements 2022-05-21 17:02:46 +02:00
p3g4asus
9bb93781d3 Fix youtube link in youtube grabber 2022-04-30 20:02:22 +02:00
p3g4asus
b7e74575f6 Improving and simplifying shared files path management 2022-04-27 17:59:10 +02:00
p3g4asus
de320977d2 Fixed m3i speed bug
Better chose writable folder for android content intents
Fixed training program load from other folders
When loading training programs fails, a window is showed with error message
2022-04-23 14:05:39 +02:00
p3g4asus
e17c44bd80 Fixed functionality of Load Settings Other folders button 2022-04-21 16:51:37 +02:00
p3g4asus
d877556f47 Fixes bind bug 2022-04-18 17:45:50 +02:00
p3g4asus
43e72bb876 Better compatibility with nginx proxy 2022-04-15 18:40:49 +02:00
p3g4asus
815b94809a youtube-viewer: added support to skrepin end peertube video providers
QZ: on settings save QZ will now ask to share the settings file
To load settings you can use any file manager to share qzs file to QZ. If QZ was not previously open, it will be launched and will not need to be restarted.
2022-03-13 10:38:48 +01:00
37 changed files with 2514 additions and 59 deletions

6
src/Constants.qml Normal file
View File

@@ -0,0 +1,6 @@
pragma Singleton
import QtQuick 2.6
QtObject {
readonly property int SHARE_REQUEST_CODE: 131313
}

View File

@@ -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>

View File

@@ -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'

View File

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

View 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

View File

@@ -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());
}
}

View 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;
}
}

View File

@@ -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(); }

View File

@@ -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);

View File

@@ -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>

View 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

View 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));
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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>

View 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

View 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

View 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

View 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

View 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

View 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

View 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); }

View 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

View File

@@ -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;

View File

@@ -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;

View 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));
});

View 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);
}
}
}

View File

@@ -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',

View File

@@ -84,6 +84,8 @@
<div class="dropdown-menu">
<a class="dropdown-item" data-grabber="youtube" href="#"><i class="fab fa-youtube"></i>&nbsp;&nbsp;Youtube</a>
<a class="dropdown-item" data-grabber="twitch" href="#"><i class="fab fa-twitch"></i>&nbsp;&nbsp;Twitch</a>
<a class="dropdown-item" data-grabber="peertube" href="#"><i class="fa-solid fa-play"></i>&nbsp;&nbsp;Peertube</a>
<a class="dropdown-item" data-grabber="skrepin" href="#"><i class="fa-solid fa-play"></i>&nbsp;&nbsp;Skrepin</a>
<a class="dropdown-item" data-grabber="personal" href="#"><i class="fa fa-border-none"></i>&nbsp;&nbsp;Personal</a>
</div>
</div>

View File

@@ -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;

View File

@@ -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;

View 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));
});

View File

@@ -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');
}

View File

@@ -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);
}

View File

@@ -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)