├── .gitignore ├── AUTHOR.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── _temp └── AndroidManifest_5_13_2.xml ├── android ├── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── res │ ├── values │ │ └── libs.xml │ └── xml │ │ └── filepaths.xml └── src │ └── org │ └── ekkescorner │ ├── examples │ └── sharex │ │ └── QShareActivity.java │ └── utils │ ├── QSharePathResolver.java │ └── QShareUtils.java ├── cpp ├── android │ ├── androidshareutils.cpp │ └── androidshareutils.hpp ├── applicationui.cpp ├── applicationui.hpp ├── ios │ ├── docviewcontroller.hpp │ └── iosshareutils.hpp ├── main.cpp ├── shareutils.cpp └── shareutils.hpp ├── data_assets.qrc ├── data_assets ├── crete.jpg ├── qt-logo.png ├── share_file.pdf └── test.docx ├── deployment.pri ├── docs ├── android_share_chooser.png ├── android_share_send_chooser.png ├── file_flow.png ├── handle_url_from_ios_apps.png ├── ios_preview.png ├── ios_share.png ├── new_intent.png ├── process_intent.png ├── qt_blog_overview.png ├── share_overview.png └── share_overview_v2.png ├── ios ├── Info.plist └── src │ ├── docviewcontroller.mm │ └── iosshareutils.mm ├── qml.qrc ├── qml └── main.qml └── share_example_x.pro /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | #QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | *.qm 37 | 38 | android/local.properties 39 | 40 | asset_catalog_compiler.Info.plist 41 | 42 | ios_signature.pri 43 | 44 | -------------------------------------------------------------------------------- /AUTHOR.md: -------------------------------------------------------------------------------- 1 | # Share Example APP for Qt 5.9+ 2 | If not otherwise mentioned inside source files (*.cpp, *.hpp, *.qml): 3 | 4 | ##Author: 5 | ekke (Ekkehard Gentz) 6 | 7 | Max-Josefs-Platz 30 8 | 9 | 83022 Rosenheim 10 | 11 | Germany 12 | 13 | Twitter: @ekkescorner 14 | 15 | LinkedIn: https://de.linkedin.com/in/ekkehard 16 | 17 | ###blogs 18 | http://ekkes-corner.org 19 | 20 | http://appbus.org 21 | 22 | http://j.mp/qt-x 23 | 24 | (c) 2017 ekke (ekkehard gentz) 25 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | My blog about Qt for mobile: http://j.mp/qt-x 3 | 4 | This project got first ideas from http://blog.lasconic.com/share-on-ios-and-android-using-qml/ 5 | see also github project: https://github.com/lasconic/ShareUtils-QML 6 | License of github.com/lasconic/ShareUtils-QML Origin src files: 7 | ============================================================================= 8 | Copyright (c) 2014 Nicolas Froment 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | ============================================================================= 28 | 29 | also got ideas from: 30 | https://www.androidcode.ninja/android-share-intent-example/ 31 | https://www.calligra.org/blogs/sharing-with-qt-on-android/ 32 | https://stackoverflow.com/questions/7156932/open-file-in-another-app 33 | http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData 34 | https://stackoverflow.com/questions/7156932/open-file-in-another-app 35 | https://bugreports.qt.io/browse/QTBUG-42942?focusedCommentId=285903&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-285903 36 | https://stackoverflow.com/questions/5734678/custom-filtering-of-intent-chooser-based-on-installed-android-package-name 37 | 38 | Sharing Files from other iOS Apps I got the ideas and some code contribution from: 39 | Thomas K. Fischer (@taskfabric) - http://taskfabric.com - thx 40 | 41 | this app is using the unlicense: 42 | ============================================================================= 43 | This is free and unencumbered software released into the public domain. 44 | 45 | Anyone is free to copy, modify, publish, use, compile, sell, or 46 | distribute this software, either in source code form or as a compiled 47 | binary, for any purpose, commercial or non-commercial, and by any 48 | means. 49 | 50 | In jurisdictions that recognize copyright laws, the author or authors 51 | of this software dedicate any and all copyright interest in the 52 | software to the public domain. We make this dedication for the benefit 53 | of the public at large and to the detriment of our heirs and 54 | successors. We intend this dedication to be an overt act of 55 | relinquishment in perpetuity of all present and future rights to this 56 | software under copyright law. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 59 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 60 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 61 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 62 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 63 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 64 | OTHER DEALINGS IN THE SOFTWARE. 65 | 66 | For more information, please refer to 67 | ============================================================================= 68 | 69 | QSharePathResolver.java based on: https://github.com/wkh237/react-native-fetch-blob/blob/master/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java 70 | original copyright: Copyright (c) 2017 xeiyan@gmail.com 71 | src slightly modified to be used into Qt Projects: (c) 2017 ekke@ekkes-corner.org 72 | 73 | MIT License, see: https://github.com/wkh237/react-native-fetch-blob/blob/master/LICENSE: 74 | Copyright (c) 2017 xeiyan@gmail.com 75 | 76 | Permission is hereby granted, free of charge, to any person obtaining a copy 77 | of this software and associated documentation files (the "Software"), to deal 78 | in the Software without restriction, including without limitation the rights 79 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 80 | copies of the Software, and to permit persons to whom the Software is 81 | furnished to do so, subject to the following conditions: 82 | 83 | The above copyright notice and this permission notice shall be included in all 84 | copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 87 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 88 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 89 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 90 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 91 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 92 | SOFTWARE. 93 | 94 | --------------------------------------------------------------------------- 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | 4 | This is free and unencumbered software released into the public domain. 5 | 6 | Anyone is free to copy, modify, publish, use, compile, sell, or 7 | distribute this software, either in source code form or as a compiled 8 | binary, for any purpose, commercial or non-commercial, and by any 9 | means. 10 | 11 | In jurisdictions that recognize copyright laws, the author or authors 12 | of this software dedicate any and all copyright interest in the 13 | software to the public domain. We make this dedication for the benefit 14 | of the public at large and to the detriment of our heirs and 15 | successors. We intend this dedication to be an overt act of 16 | relinquishment in perpetuity of all present and future rights to this 17 | software under copyright law. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 23 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 24 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | For more information, please refer to 28 | 29 | ------------------------- 30 | some parts and ideas HowTo structure the Qt project are based on: 31 | http://blog.lasconic.com/share-on-ios-and-android-using-qml/ 32 | see github project: https://github.com/lasconic/ShareUtils-QML 33 | 34 | license of origin files: 35 | ============================================================================= 36 | Copyright (c) 2014 Nicolas Froment 37 | 38 | Permission is hereby granted, free of charge, to any person obtaining a copy 39 | of this software and associated documentation files (the "Software"), to deal 40 | in the Software without restriction, including without limitation the rights 41 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 42 | copies of the Software, and to permit persons to whom the Software is 43 | furnished to do so, subject to the following conditions: 44 | 45 | The above copyright notice and this permission notice shall be included in 46 | all copies or substantial portions of the Software. 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 53 | THE SOFTWARE. 54 | ============================================================================= 55 | 56 | this project is also inspired by: 57 | https://www.androidcode.ninja/android-share-intent-example/ 58 | https://www.calligra.org/blogs/sharing-with-qt-on-android/ 59 | https://stackoverflow.com/questions/7156932/open-file-in-another-app 60 | http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData 61 | https://stackoverflow.com/questions/7156932/open-file-in-another-app 62 | https://bugreports.qt.io/browse/QTBUG-42942?focusedCommentId=285903&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-285903 63 | https://stackoverflow.com/questions/5734678/custom-filtering-of-intent-chooser-based-on-installed-android-package-name 64 | 65 | ------------------------------------------------------------------------------ 66 | QSharePathResolver.java based on: https://github.com/wkh237/react-native-fetch-blob/blob/master/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java 67 | original copyright: Copyright (c) 2017 xeiyan@gmail.com 68 | src slightly modified to be used into Qt Projects: (c) 2017 ekke@ekkes-corner.org 69 | 70 | MIT License, see: https://github.com/wkh237/react-native-fetch-blob/blob/master/LICENSE: 71 | Copyright (c) 2017 xeiyan@gmail.com 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all 81 | copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. 90 | 91 | --------------------------------------------------------------------------- 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Share Example App 2 | 3 | [AUTHOR ( ekke )](AUTHOR.md) 4 | 5 | This app is part of ekke's blog series about mobile x-platform development: 6 | http://j.mp/qt-x 7 | 8 | ## License Information 9 | [see LICENSE ( The Unlicense )](LICENSE) 10 | 11 | ## ekkes SHARE example 12 | This is not a real-life app - this app only demonstrates 13 | 14 | 1. HowTo share Files from Qt Mobile App with other Apps on Android and iOS 15 | 16 | 2. HowTo open Qt Mobile App from other Apps on Android and iOS 17 | 18 | !!! not production ready !!! 19 | 20 | ## 1. HowTo share Files from Qt Mobile App with other Apps on Android and iOS 21 | 22 | On Android we're using Intents, on iOS UIDocumentInteractionController. 23 | 24 | 25 | Developed and tested on Android 6 - 13 (API 31), iOS and Qt 5.15.13 26 | Android: This release now supports FileProvider and sets Permissions if incoming Files need, also androidx.core library used instead of support library 27 | iOS: This release supports Xcode 12 and minimum iOS 12 (required by Qt 5.15) 28 | 29 | 2023-06-13 new: using native FileDialog (Android, iOS), also HowTo grant Permissions to manage external storage on Android) 30 | 31 | Now more examplels included to share with other Apps: PNG, JPEG, DOCX, PDF 32 | 33 | Here's an Overview about the workflows, per ex. Open a File from inside your App and edit in another App outside. 34 | 35 | ![Overview](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/share_overview_v2.png) 36 | 37 | The goal of this app is to Open / View / Edit Files from your AppData Location in other Apps. But to be able to access your Files from AppData Location you first must copy them from AppData to shared UserData - per ex. DocumentsLocation. 38 | Of course at the end you need the modified File and you must delete the copy from Documentslocation, so we must watch for a SIGNAL from Android Intent or iOS UIDocumentInteractionController. 39 | 40 | ![Files from AppData to Documents and back](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/file_flow.png) 41 | 42 | ## Share (Open / Edit) or Print from QtQuickControls2 Apps 43 | 44 | ### Android Chooser to Open in... 45 | ![Android Chooser to Open in ...](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/android_share_chooser.png) 46 | 47 | ### Android Send Stream to Printer 48 | ![Android Send Stream to Printer](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/android_share_send_chooser.png) 49 | 50 | ### iOS Preview Page 51 | ![iOS Preview](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/ios_preview.png) 52 | 53 | ### iOS Share with ... 54 | ![iOS Share](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/ios_share.png) 55 | 56 | 57 | ## 2. HowTo open Qt Mobile App from other Apps on Android and iOS 58 | 59 | ### Open Qt Apps from Android Apps 60 | 61 | ![New Intent coming in from other Android App](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/new_intent.png) 62 | 63 | ![Process Intent from other Android App](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/process_intent.png) 64 | 65 | ### Open Qt Apps from iOS Apps 66 | 67 | ![Handle Url from other iOS App](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/handle_url_from_ios_apps.png) 68 | 69 | 70 | ## More Infos 71 | follow me @ekkescorner 72 | 73 | To read more please take a look at these blogs: 74 | 75 | ![ekkes Sharing Blogs at Qt Blog](https://github.com/ekke/ekkesSHAREexample/blob/master/docs/qt_blog_overview.png) 76 | 77 | Qt: Part 1: http://blog.qt.io/blog/2017/12/01/sharing-files-android-ios-qt-app/ 78 | 79 | Qt: Part 2: http://blog.qt.io/blog/2018/01/16/sharing-files-android-ios-qt-app-part-2/ 80 | 81 | Qt: part 3: http://blog.qt.io/blog/2018/02/06/sharing-files-android-ios-qt-app-part-3/ 82 | 83 | Qt: part 4: coming soon 84 | 85 | blogs at ekkes-corner: ... work in progress - stay tuned ... 86 | 87 | articles in (german) web & mobile developer magazin: ... work in progress - stay tuned ... 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /_temp/AndroidManifest_5_13_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 93 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.0.2' 9 | } 10 | } 11 | 12 | repositories { 13 | google() 14 | jcenter() 15 | } 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | dependencies { 20 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 21 | // the newest one (1.10.0) needs sdk 33 22 | implementation 'androidx.core:core:1.7.0' 23 | } 24 | 25 | android { 26 | /******************************************************* 27 | * The following variables: 28 | * - androidBuildToolsVersion, 29 | * - androidCompileSdkVersion 30 | * - qt5AndroidDir - holds the path to qt android files 31 | * needed to build any Qt application 32 | * on Android. 33 | * 34 | * are defined in gradle.properties file. This file is 35 | * updated by QtCreator and androiddeployqt tools. 36 | * Changing them manually might break the compilation! 37 | *******************************************************/ 38 | 39 | compileSdkVersion androidCompileSdkVersion.toInteger() 40 | buildToolsVersion androidBuildToolsVersion 41 | ndkVersion androidNdkVersion 42 | 43 | // Extract native libraries from the APK 44 | packagingOptions.jniLibs.useLegacyPackaging true 45 | 46 | sourceSets { 47 | main { 48 | manifest.srcFile 'AndroidManifest.xml' 49 | java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] 50 | aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] 51 | res.srcDirs = [qt5AndroidDir + '/res', 'res'] 52 | resources.srcDirs = ['resources'] 53 | renderscript.srcDirs = ['src'] 54 | assets.srcDirs = ['assets'] 55 | jniLibs.srcDirs = ['libs'] 56 | } 57 | } 58 | 59 | tasks.withType(JavaCompile) { 60 | options.incremental = true 61 | } 62 | 63 | compileOptions { 64 | sourceCompatibility JavaVersion.VERSION_1_8 65 | targetCompatibility JavaVersion.VERSION_1_8 66 | } 67 | 68 | lintOptions { 69 | abortOnError false 70 | } 71 | 72 | // Do not compress Qt binary resources file 73 | aaptOptions { 74 | noCompress 'rcc' 75 | } 76 | 77 | defaultConfig { 78 | resConfig "en" 79 | minSdkVersion = qtMinSdkVersion 80 | targetSdkVersion = qtTargetSdkVersion 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # For more details on how to configure your build environment visit 3 | # http://www.gradle.org/docs/current/userguide/build_environment.html 4 | # Specifies the JVM arguments used for the daemon process. 5 | # The setting is particularly useful for tweaking memory settings. 6 | org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 7 | 8 | # Enable building projects in parallel 9 | org.gradle.parallel=true 10 | 11 | # Gradle caching allows reusing the build artifacts from a previous 12 | # build with the same inputs. However, over time, the cache size will 13 | # grow. Uncomment the following line to enable it. 14 | #org.gradle.caching=true 15 | 16 | android.useAndroidX=true 17 | android.enableJetifier=true 18 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/res/values/libs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://download.qt.io/ministro/android/qt5/qt-5.14 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /android/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/org/ekkescorner/examples/sharex/QShareActivity.java: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) 2 | // this project is based on ideas from 3 | // http://blog.lasconic.com/share-on-ios-and-android-using-qml/ 4 | // see github project https://github.com/lasconic/ShareUtils-QML 5 | // also inspired by: 6 | // https://www.androidcode.ninja/android-share-intent-example/ 7 | // https://www.calligra.org/blogs/sharing-with-qt-on-android/ 8 | // https://stackoverflow.com/questions/7156932/open-file-in-another-app 9 | // http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData 10 | // OpenURL in At Android: got ideas from: 11 | // https://github.com/BernhardWenzel/open-url-in-qt-android 12 | // https://github.com/tobiatesan/android_intents_qt 13 | // 14 | // see also /COPYRIGHT and /LICENSE 15 | 16 | package org.ekkescorner.examples.sharex; 17 | 18 | import org.qtproject.qt5.android.QtNative; 19 | 20 | import org.qtproject.qt5.android.bindings.QtActivity; 21 | import android.os.*; 22 | import android.content.*; 23 | import android.app.*; 24 | 25 | import java.lang.String; 26 | import android.content.Intent; 27 | import java.io.File; 28 | import android.net.Uri; 29 | import android.util.Log; 30 | import android.content.ContentResolver; 31 | import android.webkit.MimeTypeMap; 32 | 33 | import org.ekkescorner.utils.*; 34 | 35 | 36 | 37 | public class QShareActivity extends QtActivity 38 | { 39 | // native - must be implemented in Cpp via JNI 40 | // 'file' scheme or resolved from 'content' scheme: 41 | public static native void setFileUrlReceived(String url); 42 | // InputStream from 'content' scheme: 43 | public static native void setFileReceivedAndSaved(String url); 44 | // 45 | public static native void fireActivityResult(int requestCode, int resultCode); 46 | // 47 | public static native boolean checkFileExists(String url); 48 | 49 | public static boolean isIntentPending; 50 | public static boolean isInitialized; 51 | public static String workingDirPath; 52 | 53 | // Use a custom Chooser without providing own App as share target ! 54 | // see QShareUtils.java createCustomChooserAndStartActivity() 55 | // Selecting your own App as target could cause AndroidOS to call 56 | // onCreate() instead of onNewIntent() 57 | // and then you are in trouble because we're using 'singleInstance' as LaunchMode 58 | // more details: my blog at Qt 59 | @Override 60 | public void onCreate(Bundle savedInstanceState) { 61 | super.onCreate(savedInstanceState); 62 | Log.d("ekkescorner", "onCreate QShareActivity"); 63 | // now we're checking if the App was started from another Android App via Intent 64 | Intent theIntent = getIntent(); 65 | if (theIntent != null){ 66 | String theAction = theIntent.getAction(); 67 | if (theAction != null){ 68 | Log.d("ekkescorner onCreate ", theAction); 69 | // QML UI not ready yet 70 | // delay processIntent(); 71 | isIntentPending = true; 72 | } 73 | } 74 | } // onCreate 75 | 76 | // WIP - trying to find a solution to survive a 2nd onCreate 77 | // ongoing discussion in QtMob (Slack) 78 | // from other Apps not respecting that you only have a singleInstance 79 | // there are problems per ex. sharing a file from Google Files App, 80 | // but working well using Xiaomi FileManager App 81 | @Override 82 | public void onDestroy() { 83 | Log.d("ekkescorner", "onDestroy QShareActivity"); 84 | // super.onDestroy(); 85 | // System.exit() closes the App before doing onCreate() again 86 | // then the App was restarted, but looses context 87 | // This works for Samsung My Files 88 | // but Google Files doesn't call onDestroy() 89 | System.exit(0); 90 | } 91 | 92 | // we start Activity with result code 93 | // to test JNI with QAndroidActivityResultReceiver you must comment or rename 94 | // this method here - otherwise you'll get wrong request or result codes 95 | @Override 96 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 97 | super.onActivityResult(requestCode, resultCode, data); 98 | // Check which request we're responding to 99 | Log.d("ekkescorner onActivityResult", "requestCode: "+requestCode); 100 | if (resultCode == RESULT_OK) { 101 | Log.d("ekkescorner onActivityResult - resultCode: ", "SUCCESS"); 102 | } else { 103 | Log.d("ekkescorner onActivityResult - resultCode: ", "CANCEL"); 104 | } 105 | 106 | // Attention using FileDialog can trigger onActivityResult 107 | // with requestCode 1305 108 | // see https://code.qt.io/cgit/qt/qtbase.git/tree/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp#n22 109 | if (requestCode == 1305) { 110 | Log.d("ekkescorner onActivityResult - requestCode 1305 (Qt FileDialog): ", "IGNORE"); 111 | return; 112 | } 113 | 114 | // hint: result comes back too fast for Action SEND 115 | // if you want to delete/move the File add a Timer w 500ms delay 116 | // see Example App main.qml - delayDeleteTimer 117 | // if you want to revoke permissions for older OS 118 | // it makes sense also do this after the delay 119 | fireActivityResult(requestCode, resultCode); 120 | } 121 | 122 | // if we are opened from other apps: 123 | @Override 124 | public void onNewIntent(Intent intent) { 125 | Log.d("ekkescorner", "onNewIntent"); 126 | super.onNewIntent(intent); 127 | setIntent(intent); 128 | // Intent will be processed, if all is initialized and Qt / QML can handle the event 129 | if(isInitialized) { 130 | processIntent(); 131 | } else { 132 | isIntentPending = true; 133 | } 134 | } // onNewIntent 135 | 136 | public void checkPendingIntents(String workingDir) { 137 | isInitialized = true; 138 | workingDirPath = workingDir; 139 | Log.d("ekkescorner", workingDirPath); 140 | if(isIntentPending) { 141 | isIntentPending = false; 142 | Log.d("ekkescorner", "checkPendingIntents: true"); 143 | processIntent(); 144 | } else { 145 | Log.d("ekkescorner", "nothingPending"); 146 | } 147 | } // checkPendingIntents 148 | 149 | // process the Intent if Action is SEND or VIEW 150 | private void processIntent(){ 151 | Intent intent = getIntent(); 152 | 153 | Uri intentUri; 154 | String intentScheme; 155 | String intentAction; 156 | // we are listening to android.intent.action.SEND or VIEW (see Manifest) 157 | if (intent.getAction().equals("android.intent.action.VIEW")){ 158 | intentAction = "VIEW"; 159 | intentUri = intent.getData(); 160 | } else if (intent.getAction().equals("android.intent.action.SEND")){ 161 | intentAction = "SEND"; 162 | Bundle bundle = intent.getExtras(); 163 | intentUri = (Uri)bundle.get(Intent.EXTRA_STREAM); 164 | } else { 165 | Log.d("ekkescorner Intent unknown action:", intent.getAction()); 166 | return; 167 | } 168 | Log.d("ekkescorner action:", intentAction); 169 | if (intentUri == null){ 170 | Log.d("ekkescorner Intent URI:", "is null"); 171 | return; 172 | } 173 | 174 | Log.d("ekkescorner Intent URI:", intentUri.toString()); 175 | 176 | // content or file 177 | intentScheme = intentUri.getScheme(); 178 | if (intentScheme == null){ 179 | Log.d("ekkescorner Intent URI Scheme:", "is null"); 180 | return; 181 | } 182 | if(intentScheme.equals("file")){ 183 | // URI as encoded string 184 | Log.d("ekkescorner Intent File URI: ", intentUri.toString()); 185 | setFileUrlReceived(intentUri.toString()); 186 | // we are done Qt can deal with file scheme 187 | return; 188 | } 189 | if(!intentScheme.equals("content")){ 190 | Log.d("ekkescorner Intent URI unknown scheme: ", intentScheme); 191 | return; 192 | } 193 | // ok - it's a content scheme URI 194 | // we will try to resolve the Path to a File URI 195 | // if this won't work or if the File cannot be opened, 196 | // we'll try to copy the file into our App working dir via InputStream 197 | // hopefully in most cases PathResolver will give a path 198 | 199 | // you need the file extension, MimeType or Name from ContentResolver ? 200 | // here's HowTo get it: 201 | Log.d("ekkescorner Intent Content URI: ", intentUri.toString()); 202 | ContentResolver cR = this.getContentResolver(); 203 | MimeTypeMap mime = MimeTypeMap.getSingleton(); 204 | String fileExtension = mime.getExtensionFromMimeType(cR.getType(intentUri)); 205 | Log.d("ekkescorner","Intent extension: "+fileExtension); 206 | String mimeType = cR.getType(intentUri); 207 | Log.d("ekkescorner"," Intent MimeType: "+mimeType); 208 | String name = QShareUtils.getContentName(cR, intentUri); 209 | if(name != null) { 210 | Log.d("ekkescorner Intent Name:", name); 211 | } else { 212 | Log.d("ekkescorner Intent Name:", "is NULL"); 213 | } 214 | String filePath; 215 | filePath = QSharePathResolver.getRealPathFromURI(this, intentUri); 216 | if(filePath == null) { 217 | Log.d("ekkescorner QSharePathResolver:", "filePath is NULL"); 218 | } else { 219 | Log.d("ekkescorner QSharePathResolver:", filePath); 220 | // to be safe check if this File Url really can be opened by Qt 221 | // there were problems with MS office apps on Android 7 222 | if (checkFileExists(filePath)) { 223 | setFileUrlReceived(filePath); 224 | // we are done Qt can deal with file scheme 225 | return; 226 | } 227 | } 228 | 229 | // trying the InputStream way: 230 | filePath = QShareUtils.createFile(cR, intentUri, workingDirPath); 231 | if(filePath == null) { 232 | Log.d("ekkescorner Intent FilePath:", "is NULL"); 233 | return; 234 | } 235 | setFileReceivedAndSaved(filePath); 236 | } // processIntent 237 | 238 | } // class QShareActivity 239 | -------------------------------------------------------------------------------- /android/src/org/ekkescorner/utils/QSharePathResolver.java: -------------------------------------------------------------------------------- 1 | // from: https://github.com/wkh237/react-native-fetch-blob/blob/master/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java 2 | // MIT License, see: https://github.com/wkh237/react-native-fetch-blob/blob/master/LICENSE 3 | // original copyright: Copyright (c) 2017 xeiyan@gmail.com 4 | // src slightly modified to be used into Qt Projects: (c) 2017 ekke@ekkes-corner.org 5 | 6 | package org.ekkescorner.utils; 7 | 8 | import android.content.Context; 9 | import android.database.Cursor; 10 | import android.net.Uri; 11 | import android.os.Build; 12 | import android.provider.DocumentsContract; 13 | import android.provider.MediaStore; 14 | import android.content.ContentUris; 15 | import android.os.Environment; 16 | import android.content.ContentResolver; 17 | import java.io.File; 18 | import java.io.InputStream; 19 | import java.io.FileOutputStream; 20 | import android.util.Log; 21 | import java.lang.NumberFormatException; 22 | 23 | public class QSharePathResolver { 24 | public static String getRealPathFromURI(final Context context, final Uri uri) { 25 | 26 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 27 | 28 | // DocumentProvider 29 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 30 | // ExternalStorageProvider 31 | if (isExternalStorageDocument(uri)) { 32 | Log.d("ekkescorner"," isExternalStorageDocument"); 33 | final String docId = DocumentsContract.getDocumentId(uri); 34 | final String[] split = docId.split(":"); 35 | final String type = split[0]; 36 | 37 | if ("primary".equalsIgnoreCase(type)) { 38 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 39 | } 40 | 41 | // TODO handle non-primary volumes 42 | } 43 | // DownloadsProvider 44 | else if (isDownloadsDocument(uri)) { 45 | Log.d("ekkescorner"," isDownloadsDocument"); 46 | final String id = DocumentsContract.getDocumentId(uri); 47 | Log.d("ekkescorner"," getDocumentId "+id); 48 | long longId = 0; 49 | try 50 | { 51 | longId = Long.valueOf(id); 52 | } 53 | catch(NumberFormatException nfe) 54 | { 55 | return getDataColumn(context, uri, null, null); 56 | } 57 | final Uri contentUri = ContentUris.withAppendedId( 58 | Uri.parse("content://downloads/public_downloads"), longId); 59 | 60 | return getDataColumn(context, contentUri, null, null); 61 | } 62 | // MediaProvider 63 | else if (isMediaDocument(uri)) { 64 | Log.d("ekkescorner"," isMediaDocument"); 65 | final String docId = DocumentsContract.getDocumentId(uri); 66 | final String[] split = docId.split(":"); 67 | final String type = split[0]; 68 | 69 | Uri contentUri = null; 70 | if ("image".equals(type)) { 71 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 72 | } else if ("video".equals(type)) { 73 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 74 | } else if ("audio".equals(type)) { 75 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 76 | } 77 | 78 | final String selection = "_id=?"; 79 | final String[] selectionArgs = new String[] { 80 | split[1] 81 | }; 82 | 83 | return getDataColumn(context, contentUri, selection, selectionArgs); 84 | } 85 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 86 | Log.d("ekkescorner"," is uri.getScheme()"); 87 | // Return the remote address 88 | if (isGooglePhotosUri(uri)) 89 | return uri.getLastPathSegment(); 90 | 91 | return getDataColumn(context, uri, null, null); 92 | } 93 | // Other Providers 94 | else{ 95 | Log.d("ekkescorner ","is Other Provider"); 96 | try { 97 | InputStream attachment = context.getContentResolver().openInputStream(uri); 98 | if (attachment != null) { 99 | String filename = getContentName(context.getContentResolver(), uri); 100 | if (filename != null) { 101 | File file = new File(context.getCacheDir(), filename); 102 | FileOutputStream tmp = new FileOutputStream(file); 103 | byte[] buffer = new byte[1024]; 104 | while (attachment.read(buffer) > 0) { 105 | tmp.write(buffer); 106 | } 107 | tmp.close(); 108 | attachment.close(); 109 | return file.getAbsolutePath(); 110 | } 111 | } 112 | } catch (Exception e) { 113 | // TODO SIGNAL shareError() 114 | return null; 115 | } 116 | } 117 | } 118 | // MediaStore (and general) 119 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 120 | Log.d("ekkescorner ","NOT DocumentsContract.isDocumentUri"); 121 | Log.d("ekkescorner"," is uri.getScheme()"); 122 | // Return the remote address 123 | if (isGooglePhotosUri(uri)) 124 | return uri.getLastPathSegment(); 125 | Log.d("ekkescorner"," return: getDataColumn "); 126 | return getDataColumn(context, uri, null, null); 127 | } 128 | // File 129 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 130 | Log.d("ekkescorner ","NOT DocumentsContract.isDocumentUri"); 131 | Log.d("ekkescorner"," is file scheme"); 132 | return uri.getPath(); 133 | } 134 | 135 | return null; 136 | } 137 | 138 | private static String getContentName(ContentResolver resolver, Uri uri) { 139 | Cursor cursor = resolver.query(uri, null, null, null, null); 140 | cursor.moveToFirst(); 141 | int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); 142 | if (nameIndex >= 0) { 143 | String name = cursor.getString(nameIndex); 144 | cursor.close(); 145 | return name; 146 | } 147 | cursor.close(); 148 | return null; 149 | } 150 | 151 | /** 152 | * Get the value of the data column for this Uri. This is useful for 153 | * MediaStore Uris, and other file-based ContentProviders. 154 | * 155 | * @param context The context. 156 | * @param uri The Uri to query. 157 | * @param selection (Optional) Filter used in the query. 158 | * @param selectionArgs (Optional) Selection arguments used in the query. 159 | * @return The value of the _data column, which is typically a file path. 160 | */ 161 | public static String getDataColumn(Context context, Uri uri, String selection, 162 | String[] selectionArgs) { 163 | 164 | Cursor cursor = null; 165 | String result = null; 166 | final String column = "_data"; 167 | final String[] projection = { 168 | column 169 | }; 170 | 171 | try { 172 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, 173 | null); 174 | if (cursor != null && cursor.moveToFirst()) { 175 | final int index = cursor.getColumnIndexOrThrow(column); 176 | result = cursor.getString(index); 177 | } 178 | } 179 | catch (Exception ex) { 180 | ex.printStackTrace(); 181 | return null; 182 | } 183 | finally { 184 | if (cursor != null) 185 | cursor.close(); 186 | } 187 | return result; 188 | } 189 | 190 | 191 | /** 192 | * @param uri The Uri to check. 193 | * @return Whether the Uri authority is ExternalStorageProvider. 194 | */ 195 | public static boolean isExternalStorageDocument(Uri uri) { 196 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 197 | } 198 | 199 | /** 200 | * @param uri The Uri to check. 201 | * @return Whether the Uri authority is DownloadsProvider. 202 | */ 203 | public static boolean isDownloadsDocument(Uri uri) { 204 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 205 | } 206 | 207 | /** 208 | * @param uri The Uri to check. 209 | * @return Whether the Uri authority is MediaProvider. 210 | */ 211 | public static boolean isMediaDocument(Uri uri) { 212 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 213 | } 214 | 215 | /** 216 | * @param uri The Uri to check. 217 | * @return Whether the Uri authority is Google Photos. 218 | */ 219 | public static boolean isGooglePhotosUri(Uri uri) { 220 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /android/src/org/ekkescorner/utils/QShareUtils.java: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) 2 | // this project is based on ideas from 3 | // http://blog.lasconic.com/share-on-ios-and-android-using-qml/ 4 | // see github project https://github.com/lasconic/ShareUtils-QML 5 | // also inspired by: 6 | // https://www.androidcode.ninja/android-share-intent-example/ 7 | // https://www.calligra.org/blogs/sharing-with-qt-on-android/ 8 | // https://stackoverflow.com/questions/7156932/open-file-in-another-app 9 | // http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData 10 | // https://stackoverflow.com/questions/5734678/custom-filtering-of-intent-chooser-based-on-installed-android-package-name 11 | // see also /COPYRIGHT and /LICENSE 12 | // (c) 2023 Ekkehard Gentz (ekke) 13 | // switched from android.support.v4.content.FileProvider to androidx.core.content.FileProvider library. 14 | // changes in build.gradle and Android Manifest 15 | // also added to gradle.properties: 16 | // android.useAndroidX=true 17 | // android.enableJetifier=true 18 | 19 | package org.ekkescorner.utils; 20 | 21 | import org.qtproject.qt5.android.QtNative; 22 | 23 | import java.lang.String; 24 | import android.content.Intent; 25 | import java.io.File; 26 | import android.net.Uri; 27 | import android.util.Log; 28 | 29 | import android.content.ContentResolver; 30 | import android.database.Cursor; 31 | import android.provider.MediaStore; 32 | import java.io.FileNotFoundException; 33 | import java.io.IOException; 34 | import java.io.InputStream; 35 | import java.io.FileOutputStream; 36 | 37 | import java.util.List; 38 | import android.content.pm.ResolveInfo; 39 | import java.util.ArrayList; 40 | import android.content.pm.PackageManager; 41 | import java.util.Comparator; 42 | import java.util.Collections; 43 | import android.content.Context; 44 | import android.os.Parcelable; 45 | 46 | import android.os.Build; 47 | 48 | import androidx.core.content.FileProvider; 49 | import androidx.core.app.ShareCompat; 50 | 51 | public class QShareUtils 52 | { 53 | // reference Authority as defined in AndroidManifest.xml 54 | private static String AUTHORITY="org.ekkescorner.examples.sharex.fileprovider"; 55 | 56 | protected QShareUtils() 57 | { 58 | //Log.d("ekkescorner", "QShareUtils()"); 59 | } 60 | 61 | public static boolean checkMimeTypeView(String mimeType) { 62 | if (QtNative.activity() == null) 63 | return false; 64 | Intent myIntent = new Intent(); 65 | myIntent.setAction(Intent.ACTION_VIEW); 66 | // without an URI resolve always fails 67 | // an empty URI allows to resolve the Activity 68 | File fileToShare = new File(""); 69 | Uri uri = Uri.fromFile(fileToShare); 70 | myIntent.setDataAndType(uri, mimeType); 71 | 72 | // Verify that the intent will resolve to an activity 73 | if (myIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { 74 | Log.d("ekkescorner checkMime ", "YEP - we can go on and View"); 75 | return true; 76 | } else { 77 | Log.d("ekkescorner checkMime", "sorry - no App available to View"); 78 | } 79 | return false; 80 | } 81 | 82 | public static boolean checkMimeTypeEdit(String mimeType) { 83 | if (QtNative.activity() == null) 84 | return false; 85 | Intent myIntent = new Intent(); 86 | myIntent.setAction(Intent.ACTION_EDIT); 87 | // without an URI resolve always fails 88 | // an empty URI allows to resolve the Activity 89 | File fileToShare = new File(""); 90 | Uri uri = Uri.fromFile(fileToShare); 91 | myIntent.setDataAndType(uri, mimeType); 92 | 93 | // Verify that the intent will resolve to an activity 94 | if (myIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { 95 | Log.d("ekkescorner checkMime ", "YEP - we can go on and Edit"); 96 | return true; 97 | } else { 98 | Log.d("ekkescorner checkMime", "sorry - no App available to Edit"); 99 | } 100 | return false; 101 | } 102 | 103 | public static boolean share(String text, String url) { 104 | if (QtNative.activity() == null) 105 | return false; 106 | Intent sendIntent = new Intent(); 107 | sendIntent.setAction(Intent.ACTION_SEND); 108 | sendIntent.putExtra(Intent.EXTRA_TEXT, text + " " + url); 109 | sendIntent.setType("text/plain"); 110 | 111 | // Verify that the intent will resolve to an activity 112 | if (sendIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { 113 | QtNative.activity().startActivity(sendIntent); 114 | return true; 115 | } else { 116 | Log.d("ekkescorner share", "Intent not resolved"); 117 | } 118 | return false; 119 | } 120 | 121 | // thx @oxied and @pooks for the idea: https://stackoverflow.com/a/18835895/135559 122 | // theIntent is already configured with all needed properties and flags 123 | // so we only have to add the packageName of targeted app 124 | public static boolean createCustomChooserAndStartActivity(Intent theIntent, String title, int requestId, Uri uri) { 125 | final Context context = QtNative.activity(); 126 | final PackageManager packageManager = context.getPackageManager(); 127 | final boolean isLowerOrEqualsKitKat = Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT; 128 | 129 | // MATCH_DEFAULT_ONLY: Resolution and querying flag. if set, only filters that support the CATEGORY_DEFAULT will be considered for matching. 130 | // Check if there is a default app for this type of content. 131 | ResolveInfo defaultAppInfo = packageManager.resolveActivity(theIntent, PackageManager.MATCH_DEFAULT_ONLY); 132 | if(defaultAppInfo == null) { 133 | Log.d("ekkescorner", title+" PackageManager cannot resolve Activity"); 134 | return false; 135 | } 136 | 137 | // had to remove this check - there can be more Activity names, per ex 138 | // com.google.android.apps.docs.editors.kix.quickword.QuickWordDocumentOpenerActivityAlias 139 | // if (!defaultAppInfo.activityInfo.name.endsWith("ResolverActivity") && !defaultAppInfo.activityInfo.name.endsWith("EditActivity")) { 140 | // Log.d("ekkescorner", title+" defaultAppInfo not Resolver or EditActivity: "+defaultAppInfo.activityInfo.name); 141 | // return false; 142 | //} 143 | 144 | // Retrieve all apps for our intent. Check if there are any apps returned 145 | List appInfoList = packageManager.queryIntentActivities(theIntent, PackageManager.MATCH_DEFAULT_ONLY); 146 | if (appInfoList.isEmpty()) { 147 | Log.d("ekkescorner", title+" appInfoList.isEmpty"); 148 | return false; 149 | } 150 | Log.d("ekkescorner", title+" appInfoList: "+appInfoList.size()); 151 | 152 | // Sort in alphabetical order 153 | Collections.sort(appInfoList, new Comparator() { 154 | @Override 155 | public int compare(ResolveInfo first, ResolveInfo second) { 156 | String firstName = first.loadLabel(packageManager).toString(); 157 | String secondName = second.loadLabel(packageManager).toString(); 158 | return firstName.compareToIgnoreCase(secondName); 159 | } 160 | }); 161 | 162 | List targetedIntents = new ArrayList(); 163 | // Filter itself and create intent with the rest of the apps. 164 | for (ResolveInfo appInfo : appInfoList) { 165 | // get the target PackageName 166 | String targetPackageName = appInfo.activityInfo.packageName; 167 | // we don't want to share with our own app 168 | // in fact sharing with own app with resultCode will crash because doesn't work well with launch mode 'singleInstance' 169 | if (targetPackageName.equals(context.getPackageName())) { 170 | continue; 171 | } 172 | // if you have a blacklist of apps please exclude them here 173 | 174 | // we create the targeted Intent based on our already configured Intent 175 | Intent targetedIntent = new Intent(theIntent); 176 | // now add the target packageName so this Intent will only find the one specific App 177 | targetedIntent.setPackage(targetPackageName); 178 | // collect all these targetedIntents 179 | targetedIntents.add(targetedIntent); 180 | 181 | // legacy support and Workaround for Android bug 182 | // grantUriPermission needed for KITKAT or older 183 | // see https://code.google.com/p/android/issues/detail?id=76683 184 | // also: https://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps 185 | 186 | // did some changes to make it run with API 30+ and Android 13 devices. 187 | // removed KitKat check and added queries to AndroidManifest 188 | // thx: https://forum.qt.io/topic/127170/android-11-qdir-mkdir-does-not-always-work/11 189 | context.grantUriPermission(targetPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 190 | /* 191 | if(isLowerOrEqualsKitKat) { 192 | Log.d("ekkescorner", "legacy support grantUriPermission"); 193 | context.grantUriPermission(targetPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 194 | // attention: you must revoke the permission later, so this only makes sense with getting back a result to know that Intent was done 195 | // I always move or delete the file, so I don't revoke permission 196 | } 197 | */ 198 | } 199 | 200 | // check if there are apps found for our Intent to avoid that there was only our own removed app before 201 | if (targetedIntents.isEmpty()) { 202 | Log.d("ekkescorner", title+" targetedIntents.isEmpty"); 203 | return false; 204 | } 205 | 206 | // now we can create our Intent with custom Chooser 207 | // we need all collected targetedIntents as EXTRA_INITIAL_INTENTS 208 | // we're using the last targetedIntent as initializing Intent, because 209 | // chooser adds its initializing intent to the end of EXTRA_INITIAL_INTENTS :) 210 | Intent chooserIntent = Intent.createChooser(targetedIntents.remove(targetedIntents.size() - 1), title); 211 | if (targetedIntents.isEmpty()) { 212 | Log.d("ekkescorner", title+" only one Intent left for Chooser"); 213 | } else { 214 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toArray(new Parcelable[] {})); 215 | } 216 | // Verify that the intent will resolve to an activity 217 | if (chooserIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { 218 | if(requestId > 0) { 219 | QtNative.activity().startActivityForResult(chooserIntent, requestId); 220 | } else { 221 | QtNative.activity().startActivity(chooserIntent); 222 | } 223 | return true; 224 | } 225 | Log.d("ekkescorner", title+" Chooser Intent not resolved. Should never happen"); 226 | return false; 227 | } 228 | 229 | // I am deleting the files from shared folder when Activity was done or canceled 230 | // so probably I don't have to revike FilePermissions for older OS 231 | // if you don't delete or move the file: here's what you must done to revoke the access 232 | public static void revokeFilePermissions(String filePath) { 233 | final Context context = QtNative.activity(); 234 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { 235 | File file = new File(filePath); 236 | Uri uri = FileProvider.getUriForFile(context, AUTHORITY, file); 237 | context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 238 | } 239 | } 240 | 241 | public static boolean sendFile(String filePath, String title, String mimeType, int requestId) { 242 | if (QtNative.activity() == null) 243 | return false; 244 | 245 | // using v4 support library create the Intent from ShareCompat 246 | // Intent sendIntent = new Intent(); 247 | Intent sendIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent(); 248 | sendIntent.setAction(Intent.ACTION_SEND); 249 | 250 | File imageFileToShare = new File(filePath); 251 | 252 | // Using FileProvider you must get the URI from FileProvider using your AUTHORITY 253 | // Uri uri = Uri.fromFile(imageFileToShare); 254 | Uri uri; 255 | try { 256 | uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare); 257 | } catch (IllegalArgumentException e) { 258 | Log.d("ekkescorner sendFile - cannot be shared: ", filePath); 259 | return false; 260 | } 261 | 262 | Log.d("ekkescorner sendFile", uri.toString()); 263 | sendIntent.putExtra(Intent.EXTRA_STREAM, uri); 264 | 265 | if(mimeType == null || mimeType.isEmpty()) { 266 | // fallback if mimeType not set 267 | mimeType = QtNative.activity().getContentResolver().getType(uri); 268 | Log.d("ekkescorner sendFile guessed mimeType:", mimeType); 269 | } else { 270 | Log.d("ekkescorner sendFile w mimeType:", mimeType); 271 | } 272 | 273 | // had to change setType into setDataAndType to avoid SecurityException 274 | // sendIntent.setType(mimeType); 275 | sendIntent.setDataAndType(uri, mimeType); 276 | 277 | sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 278 | sendIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 279 | 280 | return createCustomChooserAndStartActivity(sendIntent, title, requestId, uri); 281 | } 282 | 283 | public static boolean viewFile(String filePath, String title, String mimeType, int requestId) { 284 | if (QtNative.activity() == null) 285 | return false; 286 | 287 | // using androidx.core library create the Intent from ShareCompat 288 | // Intent viewIntent = new Intent(); 289 | Intent viewIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent(); 290 | viewIntent.setAction(Intent.ACTION_VIEW); 291 | 292 | File imageFileToShare = new File(filePath); 293 | 294 | // Using FileProvider you must get the URI from FileProvider using your AUTHORITY 295 | // Uri uri = Uri.fromFile(imageFileToShare); 296 | Uri uri; 297 | try { 298 | uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare); 299 | } catch (IllegalArgumentException e) { 300 | Log.d("ekkescorner viewFile - cannot be shared: ", filePath); 301 | return false; 302 | } 303 | // now we got a content URI per ex 304 | // content://org.ekkescorner.examples.sharex.fileprovider/my_shared_files/qt-logo.png 305 | // from a fileUrl: 306 | // /data/user/0/org.ekkescorner.examples.sharex/files/share_example_x_files/qt-logo.png 307 | Log.d("ekkescorner viewFile from file path: ", filePath); 308 | Log.d("ekkescorner viewFile to content URI: ", uri.toString()); 309 | 310 | if(mimeType == null || mimeType.isEmpty()) { 311 | // fallback if mimeType not set 312 | mimeType = QtNative.activity().getContentResolver().getType(uri); 313 | Log.d("ekkescorner viewFile guessed mimeType:", mimeType); 314 | } else { 315 | Log.d("ekkescorner viewFile w mimeType:", mimeType); 316 | } 317 | 318 | viewIntent.setDataAndType(uri, mimeType); 319 | 320 | viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 321 | viewIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 322 | 323 | return createCustomChooserAndStartActivity(viewIntent, title, requestId, uri); 324 | } 325 | 326 | public static boolean editFile(String filePath, String title, String mimeType, int requestId) { 327 | if (QtNative.activity() == null) 328 | return false; 329 | 330 | // using v4 support library create the Intent from ShareCompat 331 | // Intent editIntent = new Intent(); 332 | Intent editIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent(); 333 | editIntent.setAction(Intent.ACTION_EDIT); 334 | 335 | File imageFileToShare = new File(filePath); 336 | 337 | // Using FileProvider you must get the URI from FileProvider using your AUTHORITY 338 | // Uri uri = Uri.fromFile(imageFileToShare); 339 | Uri uri; 340 | try { 341 | uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare); 342 | } catch (IllegalArgumentException e) { 343 | Log.d("ekkescorner editFile - cannot be shared: ", filePath); 344 | return false; 345 | } 346 | Log.d("ekkescorner editFile", uri.toString()); 347 | 348 | if(mimeType == null || mimeType.isEmpty()) { 349 | // fallback if mimeType not set 350 | mimeType = QtNative.activity().getContentResolver().getType(uri); 351 | Log.d("ekkescorner editFile guessed mimeType:", mimeType); 352 | } else { 353 | Log.d("ekkescorner editFile w mimeType:", mimeType); 354 | } 355 | 356 | editIntent.setDataAndType(uri, mimeType); 357 | 358 | editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 359 | editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 360 | 361 | return createCustomChooserAndStartActivity(editIntent, title, requestId, uri); 362 | } 363 | 364 | public static String getContentName(ContentResolver cR, Uri uri) { 365 | Cursor cursor = cR.query(uri, null, null, null, null); 366 | cursor.moveToFirst(); 367 | int nameIndex = cursor 368 | .getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); 369 | if (nameIndex >= 0) { 370 | return cursor.getString(nameIndex); 371 | } else { 372 | return null; 373 | } 374 | } 375 | 376 | public static String createFile(ContentResolver cR, Uri uri, String fileLocation) { 377 | String filePath = null; 378 | try { 379 | InputStream iStream = cR.openInputStream(uri); 380 | if (iStream != null) { 381 | String name = getContentName(cR, uri); 382 | if (name != null) { 383 | filePath = fileLocation + "/" + name; 384 | Log.d("ekkescorner - create File", filePath); 385 | File f = new File(filePath); 386 | FileOutputStream tmp = new FileOutputStream(f); 387 | Log.d("ekkescorner - create File", "new FileOutputStream"); 388 | 389 | byte[] buffer = new byte[1024]; 390 | while (iStream.read(buffer) > 0) { 391 | tmp.write(buffer); 392 | } 393 | tmp.close(); 394 | iStream.close(); 395 | return filePath; 396 | } // name 397 | } // iStream 398 | } catch (FileNotFoundException e) { 399 | e.printStackTrace(); 400 | return filePath; 401 | } catch (IOException e) { 402 | e.printStackTrace(); 403 | return filePath; 404 | } catch (Exception e) { 405 | e.printStackTrace(); 406 | return filePath; 407 | } 408 | return filePath; 409 | } 410 | 411 | } 412 | -------------------------------------------------------------------------------- /cpp/android/androidshareutils.cpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #include "androidshareutils.hpp" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | const static int RESULT_OK = -1; 15 | const static int RESULT_CANCELED = 0; 16 | 17 | AndroidShareUtils* AndroidShareUtils::mInstance = NULL; 18 | 19 | AndroidShareUtils::AndroidShareUtils(QObject* parent) : PlatformShareUtils(parent) 20 | { 21 | // we need the instance for JNI Call 22 | mInstance = this; 23 | } 24 | 25 | AndroidShareUtils* AndroidShareUtils::getInstance() 26 | { 27 | if (!mInstance) { 28 | mInstance = new AndroidShareUtils; 29 | qWarning() << "AndroidShareUtils should be instantiated !"; 30 | } 31 | 32 | return mInstance; 33 | } 34 | 35 | bool AndroidShareUtils::checkMimeTypeView(const QString &mimeType) 36 | { 37 | QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType); 38 | jboolean verified = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", 39 | "checkMimeTypeView", 40 | "(Ljava/lang/String;)Z", 41 | jsMime.object()); 42 | qDebug() << "View VERIFIED: " << mimeType << " - " << verified; 43 | return verified; 44 | } 45 | 46 | bool AndroidShareUtils::checkMimeTypeEdit(const QString &mimeType) 47 | { 48 | QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType); 49 | jboolean verified = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", 50 | "checkMimeTypeEdit", 51 | "(Ljava/lang/String;)Z", 52 | jsMime.object()); 53 | qDebug() << "Edit VERIFIED: " << mimeType << " - " << verified; 54 | return verified; 55 | } 56 | 57 | void AndroidShareUtils::share(const QString &text, const QUrl &url) 58 | { 59 | QAndroidJniObject jsText = QAndroidJniObject::fromString(text); 60 | QAndroidJniObject jsUrl = QAndroidJniObject::fromString(url.toString()); 61 | jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", 62 | "share", 63 | "(Ljava/lang/String;Ljava/lang/String;)Z", 64 | jsText.object(), jsUrl.object()); 65 | 66 | if(!ok) { 67 | qWarning() << "Unable to resolve activity from Java"; 68 | emit shareNoAppAvailable(0); 69 | } 70 | } 71 | 72 | /* 73 | * As default we're going the Java - way with one simple JNI call (recommended) 74 | * if altImpl is true we're going the pure JNI way 75 | * HINT: we don't use altImpl anymore 76 | * 77 | * If a requestId was set we want to get the Activity Result back (recommended) 78 | * We need the Request Id and Result Id to control our workflow 79 | */ 80 | void AndroidShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) 81 | { 82 | mIsEditMode = false; 83 | 84 | if(!altImpl) { 85 | QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath); 86 | QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title); 87 | QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType); 88 | jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", 89 | "sendFile", 90 | "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", 91 | jsPath.object(), jsTitle.object(), jsMimeType.object(), requestId); 92 | if(!ok) { 93 | qWarning() << "Unable to resolve activity from Java"; 94 | emit shareNoAppAvailable(requestId); 95 | } 96 | return; 97 | } 98 | 99 | // THE FILE PATH 100 | // to get a valid Path we must prefix file:// 101 | // attention file must be inside Users Documents folder ! 102 | // trying to send a file from APP DATA will fail 103 | QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://"+filePath); 104 | if(!jniPath.isValid()) { 105 | qWarning() << "QAndroidJniObject jniPath not valid."; 106 | emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid")); 107 | return; 108 | } 109 | // next step: convert filePath Java String into Java Uri 110 | QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object()); 111 | if(!jniUri.isValid()) { 112 | qWarning() << "QAndroidJniObject jniUri not valid."; 113 | emit shareError(requestId, tr("Share: an Error occured\nURI not valid")); 114 | return; 115 | } 116 | 117 | // THE INTENT ACTION 118 | // create a Java String for the ACTION 119 | QAndroidJniObject jniAction = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_SEND"); 120 | if(!jniAction.isValid()) { 121 | qWarning() << "QAndroidJniObject jniParam not valid."; 122 | emit shareError(requestId, tr("Share: an Error occured")); 123 | return; 124 | } 125 | // then create the Intent Object for this Action 126 | QAndroidJniObject jniIntent("android/content/Intent","(Ljava/lang/String;)V",jniAction.object()); 127 | if(!jniIntent.isValid()) { 128 | qWarning() << "QAndroidJniObject jniIntent not valid."; 129 | emit shareError(requestId, tr("Share: an Error occured")); 130 | return; 131 | } 132 | 133 | // THE MIME TYPE 134 | if(mimeType.isEmpty()) { 135 | qWarning() << "mime type is empty"; 136 | emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty")); 137 | return; 138 | } 139 | // create a Java String for the File Type (Mime Type) 140 | QAndroidJniObject jniMimeType = QAndroidJniObject::fromString(mimeType); 141 | if(!jniMimeType.isValid()) { 142 | qWarning() << "QAndroidJniObject jniMimeType not valid."; 143 | emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid")); 144 | return; 145 | } 146 | // set Type (MimeType) 147 | QAndroidJniObject jniType = jniIntent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;", jniMimeType.object()); 148 | if(!jniType.isValid()) { 149 | qWarning() << "QAndroidJniObject jniType not valid."; 150 | emit shareError(requestId, tr("Share: an Error occured")); 151 | return; 152 | } 153 | 154 | // THE EXTRA STREAM 155 | // create a Java String for the EXTRA 156 | QAndroidJniObject jniExtra = QAndroidJniObject::getStaticObjectField("android/content/Intent", "EXTRA_STREAM"); 157 | if(!jniExtra.isValid()) { 158 | qWarning() << "QAndroidJniObject jniExtra not valid."; 159 | emit shareError(requestId, tr("Share: an Error occured")); 160 | return; 161 | } 162 | // put Extra (EXTRA_STREAM and URI) 163 | QAndroidJniObject jniExtraStreamUri = jniIntent.callObjectMethod("putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;", jniExtra.object(), jniUri.object()); 164 | // QAndroidJniObject jniExtraStreamUri = jniIntent.callObjectMethod("putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", jniExtra.object(), jniExtra.object()); 165 | if(!jniExtraStreamUri.isValid()) { 166 | qWarning() << "QAndroidJniObject jniExtraStreamUri not valid."; 167 | emit shareError(requestId, tr("Share: an Error occured")); 168 | return; 169 | } 170 | 171 | QAndroidJniObject activity = QtAndroid::androidActivity(); 172 | QAndroidJniObject packageManager = activity.callObjectMethod("getPackageManager", 173 | "()Landroid/content/pm/PackageManager;"); 174 | QAndroidJniObject componentName = jniIntent.callObjectMethod("resolveActivity", 175 | "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;", 176 | packageManager.object()); 177 | if (!componentName.isValid()) { 178 | qWarning() << "Unable to resolve activity"; 179 | emit shareNoAppAvailable(requestId); 180 | return; 181 | } 182 | 183 | if(requestId <= 0) { 184 | // we dont need a result if there's no requestId 185 | QtAndroid::startActivity(jniIntent, requestId); 186 | } else { 187 | // we have the JNI Object, know the requestId 188 | // and want the Result back into 'this' handleActivityResult(...) 189 | // attention: to test JNI with QAndroidActivityResultReceiver you must comment or rename 190 | // onActivityResult() method in QShareActivity.java - otherwise you'll get wrong request or result codes 191 | QtAndroid::startActivity(jniIntent, requestId, this); 192 | } 193 | } 194 | 195 | /* 196 | * As default we're going the Java - way with one simple JNI call (recommended) 197 | * if altImpl is true we're going the pure JNI way 198 | * HINT: we don't use altImpl anymore 199 | * 200 | * If a requestId was set we want to get the Activity Result back (recommended) 201 | * We need the Request Id and Result Id to control our workflow 202 | */ 203 | void AndroidShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) 204 | { 205 | mIsEditMode = false; 206 | 207 | if(!altImpl) { 208 | QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath); 209 | QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title); 210 | QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType); 211 | jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", 212 | "viewFile", 213 | "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", 214 | jsPath.object(), jsTitle.object(), jsMimeType.object(), requestId); 215 | if(!ok) { 216 | qWarning() << "Unable to resolve activity from Java"; 217 | emit shareNoAppAvailable(requestId); 218 | } 219 | return; 220 | } 221 | 222 | // THE FILE PATH 223 | // to get a valid Path we must prefix file:// 224 | // attention file must be inside Users Documents folder ! 225 | // trying to view or edit a file from APP DATA will fail 226 | QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://"+filePath); 227 | if(!jniPath.isValid()) { 228 | qWarning() << "QAndroidJniObject jniPath not valid."; 229 | emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid")); 230 | return; 231 | } 232 | // next step: convert filePath Java String into Java Uri 233 | QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object()); 234 | if(!jniUri.isValid()) { 235 | qWarning() << "QAndroidJniObject jniUri not valid."; 236 | emit shareError(requestId, tr("Share: an Error occured\nURI not valid")); 237 | return; 238 | } 239 | 240 | // THE INTENT ACTION 241 | // create a Java String for the ACTION 242 | QAndroidJniObject jniParam = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_VIEW"); 243 | if(!jniParam.isValid()) { 244 | qWarning() << "QAndroidJniObject jniParam not valid."; 245 | emit shareError(requestId, tr("Share: an Error occured")); 246 | return; 247 | } 248 | // then create the Intent Object for this Action 249 | QAndroidJniObject jniIntent("android/content/Intent","(Ljava/lang/String;)V",jniParam.object()); 250 | if(!jniIntent.isValid()) { 251 | qWarning() << "QAndroidJniObject jniIntent not valid."; 252 | emit shareError(requestId, tr("Share: an Error occured")); 253 | return; 254 | } 255 | 256 | // THE FILE TYPE 257 | if(mimeType.isEmpty()) { 258 | qWarning() << "mime type is empty"; 259 | emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty")); 260 | return; 261 | } 262 | // create a Java String for the File Type (Mime Type) 263 | QAndroidJniObject jniType = QAndroidJniObject::fromString(mimeType); 264 | if(!jniType.isValid()) { 265 | qWarning() << "QAndroidJniObject jniType not valid."; 266 | emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid")); 267 | return; 268 | } 269 | // set Data (the URI) and Type (MimeType) 270 | QAndroidJniObject jniResult = jniIntent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;", jniUri.object(), jniType.object()); 271 | if(!jniResult.isValid()) { 272 | qWarning() << "QAndroidJniObject jniResult not valid."; 273 | emit shareError(requestId, tr("Share: an Error occured")); 274 | return; 275 | } 276 | 277 | QAndroidJniObject activity = QtAndroid::androidActivity(); 278 | QAndroidJniObject packageManager = activity.callObjectMethod("getPackageManager", 279 | "()Landroid/content/pm/PackageManager;"); 280 | QAndroidJniObject componentName = jniIntent.callObjectMethod("resolveActivity", 281 | "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;", 282 | packageManager.object()); 283 | if (!componentName.isValid()) { 284 | qWarning() << "Unable to resolve activity"; 285 | emit shareNoAppAvailable(requestId); 286 | return; 287 | } 288 | 289 | if(requestId <= 0) { 290 | // we dont need a result if there's no requestId 291 | QtAndroid::startActivity(jniIntent, requestId); 292 | } else { 293 | // we have the JNI Object, know the requestId 294 | // and want the Result back into 'this' handleActivityResult(...) 295 | // attention: to test JNI with QAndroidActivityResultReceiver you must comment or rename 296 | // onActivityResult() method in QShareActivity.java - otherwise you'll get wrong request or result codes 297 | QtAndroid::startActivity(jniIntent, requestId, this); 298 | } 299 | } 300 | 301 | /* 302 | * As default we're going the Java - way with one simple JNI call (recommended) 303 | * if altImpl is true we're going the pure JNI way 304 | * HINT: we don't use altImpl anymore 305 | * 306 | * If a requestId was set we want to get the Activity Result back (recommended) 307 | * We need the Request Id and Result Id to control our workflow 308 | */ 309 | void AndroidShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) 310 | { 311 | mIsEditMode = true; 312 | mCurrentFilePath = filePath; 313 | QFileInfo fileInfo = QFileInfo(mCurrentFilePath); 314 | mLastModified = fileInfo.lastModified().toSecsSinceEpoch(); 315 | qDebug() << "LAST MODIFIED: " << mLastModified; 316 | 317 | if(!altImpl) { 318 | QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath); 319 | QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title); 320 | QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType); 321 | 322 | jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", 323 | "editFile", 324 | "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", 325 | jsPath.object(), jsTitle.object(), jsMimeType.object(), requestId); 326 | 327 | if(!ok) { 328 | qWarning() << "Unable to resolve activity from Java"; 329 | emit shareNoAppAvailable(requestId); 330 | } 331 | return; 332 | } 333 | 334 | // THE FILE PATH 335 | // to get a valid Path we must prefix file:// 336 | // attention file must be inside Users Documents folder ! 337 | // trying to view or edit a file from APP DATA will fail 338 | QAndroidJniObject jniPath = QAndroidJniObject::fromString("file://"+filePath); 339 | if(!jniPath.isValid()) { 340 | qWarning() << "QAndroidJniObject jniPath not valid."; 341 | emit shareError(requestId, tr("Share: an Error occured\nFilePath not valid")); 342 | return; 343 | } 344 | // next step: convert filePath Java String into Java Uri 345 | QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object()); 346 | if(!jniUri.isValid()) { 347 | qWarning() << "QAndroidJniObject jniUri not valid."; 348 | emit shareError(requestId, tr("Share: an Error occured\nURI not valid")); 349 | return; 350 | } 351 | 352 | // THE INTENT ACTION 353 | // create a Java String for the ACTION 354 | QAndroidJniObject jniParam = QAndroidJniObject::getStaticObjectField("android/content/Intent", "ACTION_EDIT"); 355 | if(!jniParam.isValid()) { 356 | qWarning() << "QAndroidJniObject jniParam not valid."; 357 | emit shareError(requestId, tr("Share: an Error occured")); 358 | return; 359 | } 360 | // then create the Intent Object for this Action 361 | QAndroidJniObject jniIntent("android/content/Intent","(Ljava/lang/String;)V",jniParam.object()); 362 | if(!jniIntent.isValid()) { 363 | qWarning() << "QAndroidJniObject jniIntent not valid."; 364 | emit shareError(requestId, tr("Share: an Error occured")); 365 | return; 366 | } 367 | 368 | // THE FILE TYPE 369 | if(mimeType.isEmpty()) { 370 | qWarning() << "mime type is empty"; 371 | emit shareError(requestId, tr("Share: an Error occured\nMimeType is empty")); 372 | return; 373 | } 374 | // create a Java String for the File Type (Mime Type) 375 | QAndroidJniObject jniType = QAndroidJniObject::fromString(mimeType); 376 | if(!jniType.isValid()) { 377 | qWarning() << "QAndroidJniObject jniType not valid."; 378 | emit shareError(requestId, tr("Share: an Error occured\nMimeType not valid")); 379 | return; 380 | } 381 | // set Data (the URI) and Type (MimeType) 382 | QAndroidJniObject jniResult = jniIntent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;", jniUri.object(), jniType.object()); 383 | if(!jniResult.isValid()) { 384 | qWarning() << "QAndroidJniObject jniResult not valid."; 385 | emit shareError(requestId, tr("Share: an Error occured")); 386 | return; 387 | } 388 | 389 | QAndroidJniObject activity = QtAndroid::androidActivity(); 390 | QAndroidJniObject packageManager = activity.callObjectMethod("getPackageManager", 391 | "()Landroid/content/pm/PackageManager;"); 392 | QAndroidJniObject componentName = jniIntent.callObjectMethod("resolveActivity", 393 | "(Landroid/content/pm/PackageManager;)Landroid/content/ComponentName;", 394 | packageManager.object()); 395 | if (!componentName.isValid()) { 396 | qWarning() << "Unable to resolve activity"; 397 | emit shareNoAppAvailable(requestId); 398 | return; 399 | } 400 | 401 | // now all is ready to start the Activity: 402 | if(requestId <= 0) { 403 | // we dont need a result if there's no requestId 404 | QtAndroid::startActivity(jniIntent, requestId); 405 | } else { 406 | // we have the JNI Object, know the requestId 407 | // and want the Result back into 'this' handleActivityResult(...) 408 | // attention: to test JNI with QAndroidActivityResultReceiver you must comment or rename 409 | // onActivityResult() method in QShareActivity.java - otherwise you'll get wrong request or result codes 410 | QtAndroid::startActivity(jniIntent, requestId, this); 411 | } 412 | } 413 | 414 | // used from QAndroidActivityResultReceiver 415 | void AndroidShareUtils::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) 416 | { 417 | Q_UNUSED(data); 418 | qDebug() << "From JNI QAndroidActivityResultReceiver: " << receiverRequestCode << "ResultCode:" << resultCode; 419 | processActivityResult(receiverRequestCode, resultCode); 420 | } 421 | 422 | // used from Activity.java onActivityResult() 423 | void AndroidShareUtils::onActivityResult(int requestCode, int resultCode) 424 | { 425 | qDebug() << "From Java Activity onActivityResult: " << requestCode << "ResultCode:" << resultCode; 426 | processActivityResult(requestCode, resultCode); 427 | } 428 | 429 | void AndroidShareUtils::processActivityResult(int requestCode, int resultCode) 430 | { 431 | // we're getting RESULT_OK only if edit is done 432 | if(resultCode == RESULT_OK) { 433 | emit shareEditDone(requestCode); 434 | } else if(resultCode == RESULT_CANCELED) { 435 | if(mIsEditMode) { 436 | // Attention: not all Apps will give you the correct ResultCode: 437 | // Google Fotos will send OK if saved and CANCELED if canceled 438 | // Some Apps always sends CANCELED even if you modified and Saved the File 439 | // so you should check the modified Timestamp of the File to know if 440 | // you should emit shareEditDone() or shareFinished() !!! 441 | QFileInfo fileInfo = QFileInfo(mCurrentFilePath); 442 | qint64 currentModified = fileInfo.lastModified().toSecsSinceEpoch(); 443 | qDebug() << "CURRENT MODIFIED: " << currentModified; 444 | if(currentModified > mLastModified) { 445 | emit shareEditDone(requestCode); 446 | return; 447 | } 448 | } 449 | emit shareFinished(requestCode); 450 | } else { 451 | qDebug() << "wrong result code: " << resultCode << " from request: " << requestCode; 452 | emit shareError(requestCode, tr("Share: an Error occured")); 453 | } 454 | } 455 | 456 | void AndroidShareUtils::checkPendingIntents(const QString workingDirPath) 457 | { 458 | QAndroidJniObject activity = QtAndroid::androidActivity(); 459 | if(activity.isValid()) { 460 | // create a Java String for the Working Dir Path 461 | QAndroidJniObject jniWorkingDir = QAndroidJniObject::fromString(workingDirPath); 462 | if(!jniWorkingDir.isValid()) { 463 | qWarning() << "QAndroidJniObject jniWorkingDir not valid."; 464 | emit shareError(0, tr("Share: an Error occured\nWorkingDir not valid")); 465 | return; 466 | } 467 | activity.callMethod("checkPendingIntents","(Ljava/lang/String;)V", jniWorkingDir.object()); 468 | qDebug() << "checkPendingIntents: " << workingDirPath; 469 | return; 470 | } 471 | qDebug() << "checkPendingIntents: Activity not valid"; 472 | } 473 | 474 | void AndroidShareUtils::setFileUrlReceived(const QString &url) 475 | { 476 | if(url.isEmpty()) { 477 | qWarning() << "setFileUrlReceived: we got an empty URL"; 478 | emit shareError(0, tr("Empty URL received")); 479 | return; 480 | } 481 | qDebug() << "AndroidShareUtils setFileUrlReceived: we got the File URL from JAVA: " << url; 482 | QString myUrl; 483 | if(url.startsWith("file://")) { 484 | myUrl= url.right(url.length()-7); 485 | qDebug() << "QFile needs this URL: " << myUrl; 486 | } else { 487 | myUrl= url; 488 | } 489 | 490 | // check if File exists 491 | QFileInfo fileInfo = QFileInfo(myUrl); 492 | if(fileInfo.exists()) { 493 | emit fileUrlReceived(myUrl); 494 | } else { 495 | qDebug() << "setFileUrlReceived: FILE does NOT exist "; 496 | emit shareError(0, tr("File does not exist: %1").arg(myUrl)); 497 | } 498 | } 499 | 500 | void AndroidShareUtils::setFileReceivedAndSaved(const QString &url) 501 | { 502 | if(url.isEmpty()) { 503 | qWarning() << "setFileReceivedAndSaved: we got an empty URL"; 504 | emit shareError(0, tr("Empty URL received")); 505 | return; 506 | } 507 | qDebug() << "AndroidShareUtils setFileReceivedAndSaved: we got the File URL from JAVA: " << url; 508 | QString myUrl; 509 | if(url.startsWith("file://")) { 510 | myUrl= url.right(url.length()-7); 511 | qDebug() << "QFile needs this URL: " << myUrl; 512 | } else { 513 | myUrl= url; 514 | } 515 | 516 | // check if File exists 517 | QFileInfo fileInfo = QFileInfo(myUrl); 518 | if(fileInfo.exists()) { 519 | emit fileReceivedAndSaved(myUrl); 520 | } else { 521 | qDebug() << "setFileReceivedAndSaved: FILE does NOT exist "; 522 | emit shareError(0, tr("File does not exist: %1").arg(myUrl)); 523 | } 524 | } 525 | 526 | // to be safe we check if a File Url from java really exists for Qt 527 | // if not on the Java side we'll try to read the content as Stream 528 | bool AndroidShareUtils::checkFileExists(const QString &url) 529 | { 530 | if(url.isEmpty()) { 531 | qWarning() << "checkFileExists: we got an empty URL"; 532 | emit shareError(0, tr("Empty URL received")); 533 | return false; 534 | } 535 | qDebug() << "AndroidShareUtils checkFileExists: we got the File URL from JAVA: " << url; 536 | QString myUrl; 537 | if(url.startsWith("file://")) { 538 | myUrl= url.right(url.length()-7); 539 | qDebug() << "QFile needs this URL: " << myUrl; 540 | } else { 541 | myUrl= url; 542 | } 543 | 544 | // check if File exists 545 | QFileInfo fileInfo = QFileInfo(myUrl); 546 | if(fileInfo.exists()) { 547 | qDebug() << "Yep: the File exists for Qt"; 548 | return true; 549 | } else { 550 | qDebug() << "Uuups: FILE does NOT exist "; 551 | return false; 552 | } 553 | } 554 | 555 | // instead of defining all JNICALL as demonstrated below 556 | // there's another way, making it easier to manage all the methods 557 | // see https://www.kdab.com/qt-android-episode-5/ 558 | 559 | #ifdef __cplusplus 560 | extern "C" { 561 | #endif 562 | 563 | JNIEXPORT void JNICALL 564 | Java_org_ekkescorner_examples_sharex_QShareActivity_setFileUrlReceived(JNIEnv *env, 565 | jobject obj, 566 | jstring url) 567 | { 568 | const char *urlStr = env->GetStringUTFChars(url, NULL); 569 | Q_UNUSED (obj) 570 | AndroidShareUtils::getInstance()->setFileUrlReceived(urlStr); 571 | env->ReleaseStringUTFChars(url, urlStr); 572 | return; 573 | } 574 | 575 | JNIEXPORT void JNICALL 576 | Java_org_ekkescorner_examples_sharex_QShareActivity_setFileReceivedAndSaved(JNIEnv *env, 577 | jobject obj, 578 | jstring url) 579 | { 580 | const char *urlStr = env->GetStringUTFChars(url, NULL); 581 | Q_UNUSED (obj) 582 | AndroidShareUtils::getInstance()->setFileReceivedAndSaved(urlStr); 583 | env->ReleaseStringUTFChars(url, urlStr); 584 | return; 585 | } 586 | 587 | JNIEXPORT bool JNICALL 588 | Java_org_ekkescorner_examples_sharex_QShareActivity_checkFileExists(JNIEnv *env, 589 | jobject obj, 590 | jstring url) 591 | { 592 | const char *urlStr = env->GetStringUTFChars(url, NULL); 593 | Q_UNUSED (obj) 594 | bool exists = AndroidShareUtils::getInstance()->checkFileExists(urlStr); 595 | env->ReleaseStringUTFChars(url, urlStr); 596 | return exists; 597 | } 598 | 599 | JNIEXPORT void JNICALL 600 | Java_org_ekkescorner_examples_sharex_QShareActivity_fireActivityResult(JNIEnv *env, 601 | jobject obj, 602 | jint requestCode, 603 | jint resultCode) 604 | { 605 | Q_UNUSED (obj) 606 | Q_UNUSED (env) 607 | AndroidShareUtils::getInstance()->onActivityResult(requestCode, resultCode); 608 | return; 609 | } 610 | 611 | #ifdef __cplusplus 612 | } 613 | #endif 614 | -------------------------------------------------------------------------------- /cpp/android/androidshareutils.hpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #ifndef ANDROIDSHAREUTILS_H 6 | #define ANDROIDSHAREUTILS_H 7 | 8 | #include 9 | #include 10 | 11 | #include "cpp/shareutils.hpp" 12 | 13 | class AndroidShareUtils : public PlatformShareUtils, public QAndroidActivityResultReceiver 14 | { 15 | public: 16 | AndroidShareUtils(QObject* parent = nullptr); 17 | bool checkMimeTypeView(const QString &mimeType) override; 18 | bool checkMimeTypeEdit(const QString &mimeType) override; 19 | void share(const QString &text, const QUrl &url) override; 20 | void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) override; 21 | void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) override; 22 | void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) override; 23 | 24 | void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override; 25 | void onActivityResult(int requestCode, int resultCode); 26 | 27 | void checkPendingIntents(const QString workingDirPath) override; 28 | 29 | static AndroidShareUtils* getInstance(); 30 | 31 | public slots: 32 | void setFileUrlReceived(const QString &url); 33 | void setFileReceivedAndSaved(const QString &url); 34 | bool checkFileExists(const QString &url); 35 | 36 | private: 37 | bool mIsEditMode; 38 | qint64 mLastModified; 39 | QString mCurrentFilePath; 40 | 41 | static AndroidShareUtils* mInstance; 42 | 43 | void processActivityResult(int requestCode, int resultCode); 44 | 45 | }; 46 | 47 | 48 | #endif // ANDROIDSHAREUTILS_H 49 | -------------------------------------------------------------------------------- /cpp/applicationui.cpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #include "applicationui.hpp" 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #if defined(Q_OS_ANDROID) 16 | #include 17 | #endif 18 | 19 | const QString IMAGE_DATA_FILE = "/qt-logo.png"; 20 | const QString IMAGE_ASSETS_FILE_PATH = ":/data_assets/qt-logo.png"; 21 | 22 | const QString JPEG_DATA_FILE = "/crete.jpg"; 23 | const QString JPEG_ASSETS_FILE_PATH = ":/data_assets/crete.jpg"; 24 | 25 | const QString DOCX_DATA_FILE = "/test.docx"; 26 | const QString DOCX_ASSETS_FILE_PATH = ":/data_assets/test.docx"; 27 | 28 | const QString PDF_DATA_FILE = "/share_file.pdf"; 29 | const QString PDF_ASSETS_FILE_PATH = ":/data_assets/share_file.pdf"; 30 | 31 | const static int NO_RESPONSE_IMAGE = 0; 32 | const static int NO_RESPONSE_PDF = -1; 33 | const static int NO_RESPONSE_JPEG = -2; 34 | const static int NO_RESPONSE_DOCX = -3; 35 | 36 | const static int EDIT_FILE_IMAGE = 42; 37 | const static int EDIT_FILE_PDF = 44; 38 | const static int EDIT_FILE_JPEG = 45; 39 | const static int EDIT_FILE_DOCX = 46; 40 | 41 | const static int VIEW_FILE_IMAGE = 22; 42 | const static int VIEW_FILE_PDF = 21; 43 | const static int VIEW_FILE_JPEG = 23; 44 | const static int VIEW_FILE_DOCX = 24; 45 | 46 | const static int SEND_FILE_IMAGE = 11; 47 | const static int SEND_FILE_PDF = 10; 48 | const static int SEND_FILE_JPEG = 12; 49 | const static int SEND_FILE_DOCX = 13; 50 | 51 | ApplicationUI::ApplicationUI(QObject *parent) : QObject(parent), mShareUtils(new ShareUtils(this)), mPendingIntentsChecked(false) 52 | { 53 | // this is a demo application where we deal with an Image and a PDF as example 54 | // Image and PDF are delivered as qrc:/ resources at /data_assets 55 | // to start the tests as first we must copy these 2 files from assets into APP DATA 56 | // so we can simulate HowTo view, edit or send files from inside your APP DATA to other APPs 57 | // in a real life app you'll have your own workflows 58 | // I made copyAssetsToAPPData() INVOKABLE to be able to reset to origin files 59 | copyAssetsToAPPData(); 60 | } 61 | 62 | void ApplicationUI::addContextProperty(QQmlContext *context) 63 | { 64 | context->setContextProperty("shareUtils", mShareUtils); 65 | } 66 | 67 | void ApplicationUI::copyAssetsToAPPData() { 68 | // Android: HomeLocation works, iOS: not writable - so I'm using always QStandardPaths::AppDataLocation 69 | // Android: AppDataLocation works out of the box, iOS you must create the DIR first !! 70 | QString appDataRoot = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).value(0); 71 | // QString appDataRoot = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0); 72 | qDebug() << "QStandardPaths::AppDataLocation: " << appDataRoot; 73 | #if defined (Q_OS_IOS) 74 | if (!QDir(appDataRoot).exists()) { 75 | if (QDir("").mkpath(appDataRoot)) { 76 | qDebug() << "Created app data directory. " << appDataRoot; 77 | } else { 78 | qWarning() << "Failed to create app data directory. " << appDataRoot; 79 | return; 80 | } 81 | } 82 | #endif 83 | // as next we create a /my_share_files subdirectory to store our example files from assets 84 | mAppDataFilesPath = appDataRoot.append("/my_share_files"); 85 | if (!QDir(mAppDataFilesPath).exists()) { 86 | if (QDir("").mkpath(mAppDataFilesPath)) { 87 | qDebug() << "Created app data /files directory. " << mAppDataFilesPath; 88 | } else { 89 | qWarning() << "Failed to create app data /files directory. " << mAppDataFilesPath; 90 | return; 91 | } 92 | } 93 | // now copy files from assets to APP DATA /my_share_files 94 | // if not existing 95 | // in real-world app you would download files from a server or so 96 | if(!QFile::exists(mAppDataFilesPath+IMAGE_DATA_FILE)) { 97 | bool copied = copyAssetFile(IMAGE_ASSETS_FILE_PATH, mAppDataFilesPath+IMAGE_DATA_FILE); 98 | if(!copied) { 99 | return; 100 | } 101 | qDebug() << "copied the Image (PNG) from Assets to APP DATA"; 102 | } 103 | if(!QFile::exists(mAppDataFilesPath+JPEG_DATA_FILE)) { 104 | bool copied = copyAssetFile(JPEG_ASSETS_FILE_PATH, mAppDataFilesPath+JPEG_DATA_FILE); 105 | if(!copied) { 106 | return; 107 | } 108 | qDebug() << "copied the Image (JPEG) from Assets to APP DATA"; 109 | } 110 | if(!QFile::exists(mAppDataFilesPath+DOCX_DATA_FILE)) { 111 | bool copied = copyAssetFile(DOCX_ASSETS_FILE_PATH, mAppDataFilesPath+DOCX_DATA_FILE); 112 | if(!copied) { 113 | return; 114 | } 115 | qDebug() << "copied the Document (DOCX) from Assets to APP DATA"; 116 | } 117 | if(!QFile::exists(mAppDataFilesPath+PDF_DATA_FILE)) { 118 | bool copied = copyAssetFile(PDF_ASSETS_FILE_PATH, mAppDataFilesPath+PDF_DATA_FILE); 119 | if(!copied) { 120 | return; 121 | } 122 | qDebug() << "copied the PDF from Assets to APP DATA"; 123 | } 124 | // to provide files to other apps we're using a specific folder 125 | // version 1 of this example used QStandardPaths::DocumentsLocation on Android and iOS 126 | // iOS: QStandardPaths::DocumentsLocation points to: /Documents - so it's inside the sandbox 127 | // Android: QStandardPaths::DocumentsLocation points to: /Documents outside the app sandbox 128 | // this worked while using FileUrl (SDK 23) 129 | // Android > SDK 23 needs a FileProvider providing a contentUrl 130 | // FileProvider uses Paths (see android/res/xml/filepaths.xml) stored at QStandardPaths::AppDataLocation 131 | 132 | // now create the working dir if not exists 133 | #if defined (Q_OS_IOS) 134 | QString docLocationRoot = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0); 135 | qDebug() << "iOS: QStandardPaths::DocumentsLocation: " << docLocationRoot; 136 | #elif defined(Q_OS_ANDROID) 137 | QString docLocationRoot = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).value(0); 138 | qDebug() << "Android: QStandardPaths::AppDataLocation: " << docLocationRoot; 139 | #else 140 | QString docLocationRoot = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0); 141 | #endif 142 | mDocumentsWorkPath = docLocationRoot.append("/share_example_x_files"); 143 | if (!QDir(mDocumentsWorkPath).exists()) { 144 | if (QDir("").mkpath(mDocumentsWorkPath)) { 145 | qDebug() << "Created Documents Location work directory. " << mDocumentsWorkPath; 146 | } else { 147 | qWarning() << "Failed to create Documents Location work directory. " << mDocumentsWorkPath; 148 | return; 149 | } 150 | } 151 | qDebug() << "Documents Location work directory exists: " << mDocumentsWorkPath; 152 | } 153 | 154 | bool ApplicationUI::copyAssetFile(const QString sourceFilePath, const QString destinationFilePath) { 155 | if (QFile::exists(destinationFilePath)) 156 | { 157 | bool removed = QFile::remove(destinationFilePath); 158 | if(!removed) { 159 | qWarning() << "Failed to remove " << destinationFilePath; 160 | return false; 161 | } 162 | } 163 | bool copied = QFile::copy(sourceFilePath, destinationFilePath); 164 | if(!copied) { 165 | qWarning() << "Failed to copy " << sourceFilePath << " to " << destinationFilePath; 166 | return false; 167 | } 168 | // because files are copied from assets it's a good idea to set r/w permissions 169 | bool permissionsSet = QFile(destinationFilePath).setPermissions(QFileDevice::ReadUser | QFileDevice::WriteUser); 170 | if(!permissionsSet) { 171 | qDebug() << "cannot set Permissions to read / write settings for " << destinationFilePath; 172 | return false; 173 | } 174 | return true; 175 | } 176 | 177 | // the old workflow (SDK 23, FilePath): 178 | // Data files in AppDataLocation cannot shared with other APPs 179 | // so we copy them into our working directory inside USERS DOCUMENTS location 180 | 181 | // the new workflow: 182 | // now with FileProvider our working directory is inside AppDataLocation 183 | QString ApplicationUI::filePathDocumentsLocation(const int requestId) { 184 | QString sourceFilePath; 185 | QString destinationFilePath; 186 | switch (requestId) { 187 | case SEND_FILE_IMAGE: 188 | case VIEW_FILE_IMAGE: 189 | case EDIT_FILE_IMAGE: 190 | case NO_RESPONSE_IMAGE: 191 | sourceFilePath = mAppDataFilesPath+IMAGE_DATA_FILE; 192 | destinationFilePath = mDocumentsWorkPath+IMAGE_DATA_FILE; 193 | break; 194 | case SEND_FILE_JPEG: 195 | case VIEW_FILE_JPEG: 196 | case EDIT_FILE_JPEG: 197 | case NO_RESPONSE_JPEG: 198 | sourceFilePath = mAppDataFilesPath+JPEG_DATA_FILE; 199 | destinationFilePath = mDocumentsWorkPath+JPEG_DATA_FILE; 200 | break; 201 | case SEND_FILE_DOCX: 202 | case VIEW_FILE_DOCX: 203 | case EDIT_FILE_DOCX: 204 | case NO_RESPONSE_DOCX: 205 | sourceFilePath = mAppDataFilesPath+DOCX_DATA_FILE; 206 | destinationFilePath = mDocumentsWorkPath+DOCX_DATA_FILE; 207 | break; 208 | default: 209 | sourceFilePath = mAppDataFilesPath+PDF_DATA_FILE; 210 | destinationFilePath = mDocumentsWorkPath+PDF_DATA_FILE; 211 | break; 212 | } 213 | // if(requestId == SEND_FILE_IMAGE || requestId == VIEW_FILE_IMAGE || requestId == EDIT_FILE_IMAGE || requestId == NO_RESPONSE_IMAGE) { 214 | // sourceFilePath = mAppDataFilesPath+IMAGE_DATA_FILE; 215 | // destinationFilePath = mDocumentsWorkPath+IMAGE_DATA_FILE; 216 | // } else { 217 | // sourceFilePath = mAppDataFilesPath+PDF_DATA_FILE; 218 | // destinationFilePath = mDocumentsWorkPath+PDF_DATA_FILE; 219 | // } 220 | if (QFile::exists(destinationFilePath)) 221 | { 222 | bool removed = QFile::remove(destinationFilePath); 223 | if(!removed) { 224 | qWarning() << "Failed to remove " << destinationFilePath; 225 | return destinationFilePath; 226 | } 227 | } 228 | bool copied = QFile::copy(sourceFilePath, destinationFilePath); 229 | if(!copied) { 230 | qWarning() << "Failed to copy " << sourceFilePath << " to " << destinationFilePath; 231 | //#if defined(Q_OS_ANDROID) 232 | // emit noDocumentsWorkLocation(); 233 | //#endif 234 | } 235 | return destinationFilePath; 236 | } 237 | 238 | bool ApplicationUI::deleteFromDocumentsLocation(const int requestId) { 239 | QString filePath; 240 | switch (requestId) { 241 | case SEND_FILE_IMAGE: 242 | case VIEW_FILE_IMAGE: 243 | case EDIT_FILE_IMAGE: 244 | case NO_RESPONSE_IMAGE: 245 | filePath = mDocumentsWorkPath+IMAGE_DATA_FILE; 246 | break; 247 | case SEND_FILE_JPEG: 248 | case VIEW_FILE_JPEG: 249 | case EDIT_FILE_JPEG: 250 | case NO_RESPONSE_JPEG: 251 | filePath = mDocumentsWorkPath+JPEG_DATA_FILE; 252 | break; 253 | case SEND_FILE_DOCX: 254 | case VIEW_FILE_DOCX: 255 | case EDIT_FILE_DOCX: 256 | case NO_RESPONSE_DOCX: 257 | filePath = mDocumentsWorkPath+DOCX_DATA_FILE; 258 | break; 259 | default: 260 | filePath = mDocumentsWorkPath+PDF_DATA_FILE; 261 | break; 262 | } 263 | // if(requestId == SEND_FILE_IMAGE || requestId == VIEW_FILE_IMAGE || requestId == EDIT_FILE_IMAGE || requestId == NO_RESPONSE_IMAGE) { 264 | // filePath = mDocumentsWorkPath+IMAGE_DATA_FILE; 265 | // } else { 266 | // filePath = mDocumentsWorkPath+PDF_DATA_FILE; 267 | // } 268 | if (QFile::exists(filePath)) { 269 | bool removed = QFile::remove(filePath); 270 | if(!removed) { 271 | qWarning() << "Failed to remove " << filePath; 272 | return false; 273 | } 274 | } else { 275 | qWarning() << "No file to delete found: " << filePath; 276 | return false; 277 | } 278 | qDebug() << "File removed from Documents Location: " << filePath; 279 | return true; 280 | } 281 | 282 | bool ApplicationUI::updateFileFromDocumentsLocation(const int requestId) { 283 | QString docLocationFilePath; 284 | QString appDataFilePath; 285 | switch (requestId) { 286 | case SEND_FILE_IMAGE: 287 | case VIEW_FILE_IMAGE: 288 | case EDIT_FILE_IMAGE: 289 | case NO_RESPONSE_IMAGE: 290 | docLocationFilePath = mDocumentsWorkPath+IMAGE_DATA_FILE; 291 | appDataFilePath = mAppDataFilesPath+IMAGE_DATA_FILE; 292 | break; 293 | case SEND_FILE_JPEG: 294 | case VIEW_FILE_JPEG: 295 | case EDIT_FILE_JPEG: 296 | case NO_RESPONSE_JPEG: 297 | docLocationFilePath = mDocumentsWorkPath+JPEG_DATA_FILE; 298 | appDataFilePath = mAppDataFilesPath+JPEG_DATA_FILE; 299 | break; 300 | case SEND_FILE_DOCX: 301 | case VIEW_FILE_DOCX: 302 | case EDIT_FILE_DOCX: 303 | case NO_RESPONSE_DOCX: 304 | docLocationFilePath = mDocumentsWorkPath+DOCX_DATA_FILE; 305 | appDataFilePath = mAppDataFilesPath+DOCX_DATA_FILE; 306 | break; 307 | default: 308 | docLocationFilePath = mDocumentsWorkPath+PDF_DATA_FILE; 309 | appDataFilePath = mAppDataFilesPath+PDF_DATA_FILE; 310 | break; 311 | } 312 | // if(requestId == SEND_FILE_IMAGE || requestId == VIEW_FILE_IMAGE || requestId == EDIT_FILE_IMAGE || requestId == NO_RESPONSE_IMAGE) { 313 | // docLocationFilePath = mDocumentsWorkPath+IMAGE_DATA_FILE; 314 | // appDataFilePath = mAppDataFilesPath+IMAGE_DATA_FILE; 315 | // } else { 316 | // docLocationFilePath = mDocumentsWorkPath+PDF_DATA_FILE; 317 | // appDataFilePath = mAppDataFilesPath+PDF_DATA_FILE; 318 | // } 319 | if (QFile::exists(docLocationFilePath)) { 320 | // delete appDataFilePath should exist 321 | if(QFile::exists(appDataFilePath)) { 322 | bool removed = QFile::remove(appDataFilePath); 323 | if(!removed) { 324 | qWarning() << "Failed to remove " << appDataFilePath; 325 | // go on 326 | } else { 327 | qDebug() << "old file removed: " << appDataFilePath; 328 | } 329 | } 330 | // now copy the file from doc location to app data location 331 | bool copied = QFile::copy(docLocationFilePath, appDataFilePath); 332 | if(!copied) { 333 | qWarning() << "Failed to copy " << docLocationFilePath << " to " << appDataFilePath; 334 | return false; 335 | } else { 336 | qDebug() << "successfully replaced " << appDataFilePath << " from " << docLocationFilePath; 337 | // now delete from Documents location 338 | bool removed = QFile::remove(docLocationFilePath); 339 | if(!removed) { 340 | qWarning() << "Failed to remove " << docLocationFilePath; 341 | // go on 342 | } else { 343 | qDebug() << "doc file removed: " << docLocationFilePath; 344 | } 345 | } 346 | } else { 347 | qWarning() << "No file to update from found: " << docLocationFilePath; 348 | return false; 349 | } 350 | return true; 351 | } 352 | 353 | #if defined(Q_OS_ANDROID) 354 | void ApplicationUI::onApplicationStateChanged(Qt::ApplicationState applicationState) 355 | { 356 | qDebug() << "S T A T E changed into: " << applicationState; 357 | if(applicationState == Qt::ApplicationState::ApplicationSuspended) { 358 | // nothing to do 359 | return; 360 | } 361 | if(applicationState == Qt::ApplicationState::ApplicationActive) { 362 | // if App was launched from VIEW or SEND Intent 363 | // there's a race collision: the event will be lost, 364 | // because App and UI wasn't completely initialized 365 | // workaround: QShareActivity remembers that an Intent is pending 366 | if(!mPendingIntentsChecked) { 367 | mPendingIntentsChecked = true; 368 | mShareUtils->checkPendingIntents(mAppDataFilesPath); 369 | } 370 | } 371 | } 372 | // we don't need permissions if we only share files to other apps using FileProvider 373 | // but we need permissions if other apps share their files with out app and we must access those files 374 | bool ApplicationUI::checkPermission() { 375 | QtAndroid::PermissionResult r = QtAndroid::checkPermission("android.permission.WRITE_EXTERNAL_STORAGE"); 376 | if(r == QtAndroid::PermissionResult::Denied) { 377 | QtAndroid::requestPermissionsSync( QStringList() << "android.permission.WRITE_EXTERNAL_STORAGE" ); 378 | r = QtAndroid::checkPermission("android.permission.WRITE_EXTERNAL_STORAGE"); 379 | if(r == QtAndroid::PermissionResult::Denied) { 380 | qDebug() << "Permission denied"; 381 | emit noDocumentsWorkLocation(); 382 | return false; 383 | } 384 | } 385 | qDebug() << "YEP: Permission OK"; 386 | return true; 387 | } 388 | #endif 389 | 390 | #if defined(Q_OS_ANDROID) 391 | 392 | // to get access to all files you need a special permission 393 | // add to your Manifest 394 | // then ask user to get full access 395 | // HINT: if you want to deploy your app via Play Store, you have to ask Google to use this permission 396 | // per ex. an app like a FileManager could be valid 397 | // if your business app is running inside a MDM, you don't need to ask Google 398 | // ATTENTION: don't forget to set your package name ! 399 | // see https://forum.qt.io/topic/137019/cannot-grant-manage_external_storage-permission-on-android-11/2 400 | // see https://bugreports.qt.io/browse/QTBUG-98974?focusedCommentId=680551&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-680551 401 | void ApplicationUI::accessAllFiles() 402 | { 403 | // QOperatingSystemVersion("Android", 13.0.0) 404 | // QOperatingSystemVersion("macOS", 12.5.0) 405 | qDebug() << "current QOperatingSystemVersion:" << QOperatingSystemVersion::current(); 406 | if(QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::Android, 13)) { 407 | qDebug() << "it is Android 13 or greater !"; 408 | } 409 | if(QOperatingSystemVersion::current() < QOperatingSystemVersion(QOperatingSystemVersion::Android, 11)) { 410 | qDebug() << "it is less then Android 11 - ALL FILES permission isn't possible!"; 411 | return; 412 | } 413 | 414 | // Here you have to set your PackageName 415 | #define PACKAGE_NAME "package:org.ekkescorner.examples.sharex" 416 | jboolean value = QAndroidJniObject::callStaticMethod("android/os/Environment", "isExternalStorageManager"); 417 | if(value == false) { 418 | qDebug() << "requesting ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION"; 419 | QAndroidJniObject ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION = QAndroidJniObject::getStaticObjectField( "android/provider/Settings", "ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION","Ljava/lang/String;" ); 420 | QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION.object()); 421 | QAndroidJniObject jniPath = QAndroidJniObject::fromString(PACKAGE_NAME); 422 | QAndroidJniObject jniUri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", jniPath.object()); 423 | QAndroidJniObject jniResult = intent.callObjectMethod("setData", "(Landroid/net/Uri;)Landroid/content/Intent;", jniUri.object() ); 424 | QtAndroid::startActivity(intent, 0); 425 | } else { 426 | qDebug() << "SUCCESS ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION"; 427 | } 428 | } 429 | #endif 430 | 431 | -------------------------------------------------------------------------------- /cpp/applicationui.hpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #ifndef APPLICATIONUI_HPP 6 | #define APPLICATIONUI_HPP 7 | 8 | #include 9 | 10 | #include 11 | #include "cpp/shareutils.hpp" 12 | 13 | class ApplicationUI : public QObject 14 | { 15 | Q_OBJECT 16 | 17 | public: 18 | ApplicationUI(QObject *parent = 0); 19 | 20 | void addContextProperty(QQmlContext* context); 21 | 22 | Q_INVOKABLE 23 | void copyAssetsToAPPData(); 24 | 25 | Q_INVOKABLE 26 | QString filePathDocumentsLocation(const int requestId); 27 | 28 | Q_INVOKABLE 29 | bool deleteFromDocumentsLocation(const int requestId); 30 | 31 | Q_INVOKABLE 32 | bool updateFileFromDocumentsLocation(const int requestId); 33 | 34 | #if defined(Q_OS_ANDROID) 35 | Q_INVOKABLE 36 | bool checkPermission(); 37 | 38 | Q_INVOKABLE 39 | void accessAllFiles(); 40 | #endif 41 | 42 | signals: 43 | void noDocumentsWorkLocation(); 44 | 45 | public slots: 46 | #if defined(Q_OS_ANDROID) 47 | void onApplicationStateChanged(Qt::ApplicationState applicationState); 48 | #endif 49 | 50 | private: 51 | ShareUtils* mShareUtils; 52 | bool mPendingIntentsChecked; 53 | 54 | QString mAppDataFilesPath; 55 | QString mDocumentsWorkPath; 56 | 57 | bool copyAssetFile(const QString sourceFilePath, const QString destinationFilePath); 58 | }; 59 | 60 | #endif // APPLICATIONUI_HPP 61 | -------------------------------------------------------------------------------- /cpp/ios/docviewcontroller.hpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #ifndef DOCVIEWCONTROLLER_HPP 6 | #define DOCVIEWCONTROLLER_HPP 7 | 8 | #import 9 | #import "iosshareutils.hpp" 10 | 11 | @interface DocViewController : UIViewController 12 | 13 | @property int requestId; 14 | 15 | @property IosShareUtils *mIosShareUtils; 16 | 17 | @end 18 | 19 | 20 | 21 | #endif // DOCVIEWCONTROLLER_HPP 22 | -------------------------------------------------------------------------------- /cpp/ios/iosshareutils.hpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #ifndef __IOSSHAREUTILS_H__ 6 | #define __IOSSHAREUTILS_H__ 7 | 8 | #include "shareutils.hpp" 9 | 10 | class IosShareUtils : public PlatformShareUtils 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit IosShareUtils(QObject *parent = 0); 16 | bool checkMimeTypeView(const QString &mimeType); 17 | bool checkMimeTypeEdit(const QString &mimeType); 18 | void share(const QString &text, const QUrl &url); 19 | void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl); 20 | void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl); 21 | void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl); 22 | 23 | void handleDocumentPreviewDone(const int &requestId); 24 | 25 | public slots: 26 | void handleFileUrlReceived(const QUrl &url); 27 | 28 | 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /cpp/main.cpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "applicationui.hpp" 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 17 | QQuickStyle::setStyle("Material"); 18 | QGuiApplication app(argc, argv); 19 | 20 | app.setOrganizationName("ekkes-corner"); 21 | app.setOrganizationDomain("org.ekkescorner.share.example"); 22 | 23 | ApplicationUI appui; 24 | 25 | QQmlApplicationEngine engine; 26 | 27 | // from QML we have access to ApplicationUI as myApp 28 | QQmlContext* context = engine.rootContext(); 29 | context->setContextProperty("myApp", &appui); 30 | // some more context properties 31 | appui.addContextProperty(context); 32 | 33 | #if defined(Q_OS_ANDROID) 34 | QObject::connect(&app, &QGuiApplication::applicationStateChanged, &appui, &ApplicationUI::onApplicationStateChanged ); 35 | #endif 36 | engine.load(QUrl(QLatin1String("qrc:/qml/main.qml"))); 37 | if (engine.rootObjects().isEmpty()) 38 | return -1; 39 | 40 | return app.exec(); 41 | } 42 | -------------------------------------------------------------------------------- /cpp/shareutils.cpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #include "shareutils.hpp" 6 | #include 7 | #include 8 | 9 | #ifdef Q_OS_IOS 10 | #include "cpp/ios/iosshareutils.hpp" 11 | #endif 12 | 13 | #ifdef Q_OS_ANDROID 14 | #include "cpp/android/androidshareutils.hpp" 15 | #endif 16 | 17 | ShareUtils::ShareUtils(QObject *parent) 18 | : QObject(parent) 19 | { 20 | #if defined(Q_OS_IOS) 21 | mPlatformShareUtils = new IosShareUtils(this); 22 | #elif defined(Q_OS_ANDROID) 23 | mPlatformShareUtils = new AndroidShareUtils(this); 24 | #else 25 | mPlatformShareUtils = new PlatformShareUtils(this); 26 | #endif 27 | 28 | bool connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareEditDone, this, &ShareUtils::onShareEditDone); 29 | Q_ASSERT(connectResult); 30 | 31 | connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareFinished, this, &ShareUtils::onShareFinished); 32 | Q_ASSERT(connectResult); 33 | 34 | connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareNoAppAvailable, this, &ShareUtils::onShareNoAppAvailable); 35 | Q_ASSERT(connectResult); 36 | 37 | connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareError, this, &ShareUtils::onShareError); 38 | Q_ASSERT(connectResult); 39 | 40 | connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::fileUrlReceived, this, &ShareUtils::onFileUrlReceived); 41 | Q_ASSERT(connectResult); 42 | 43 | connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::fileReceivedAndSaved, this, &ShareUtils::onFileReceivedAndSaved); 44 | Q_ASSERT(connectResult); 45 | 46 | Q_UNUSED(connectResult); 47 | } 48 | 49 | bool ShareUtils::checkMimeTypeView(const QString &mimeType) 50 | { 51 | return mPlatformShareUtils->checkMimeTypeView(mimeType); 52 | } 53 | 54 | bool ShareUtils::checkMimeTypeEdit(const QString &mimeType) 55 | { 56 | return mPlatformShareUtils->checkMimeTypeEdit(mimeType); 57 | } 58 | 59 | void ShareUtils::share(const QString &text, const QUrl &url) 60 | { 61 | mPlatformShareUtils->share(text, url); 62 | } 63 | 64 | void ShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) 65 | { 66 | mPlatformShareUtils->sendFile(filePath, title, mimeType, requestId, altImpl); 67 | } 68 | 69 | void ShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) 70 | { 71 | mPlatformShareUtils->viewFile(filePath, title, mimeType, requestId, altImpl); 72 | } 73 | 74 | void ShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) 75 | { 76 | mPlatformShareUtils->editFile(filePath, title, mimeType, requestId, altImpl); 77 | } 78 | 79 | void ShareUtils::checkPendingIntents(const QString workingDirPath) 80 | { 81 | mPlatformShareUtils->checkPendingIntents(workingDirPath); 82 | } 83 | 84 | // testing native FileDialog 85 | bool ShareUtils::verifyFileUrl(const QString &fileUrl) 86 | { 87 | #if defined(Q_OS_ANDROID) 88 | QFileInfo fileInfo(fileUrl); 89 | qDebug() << "verifying fileUrl: " << fileUrl; 90 | qDebug() << "BASE: " << fileInfo.baseName(); 91 | return fileInfo.exists(); 92 | #endif 93 | #if defined(Q_OS_IOS) 94 | qDebug() << "verify iOS File from assets-library " << fileUrl; 95 | 96 | QUrl url(fileUrl); 97 | QString iosFile = url.toLocalFile(); 98 | qDebug() << "converted to LocaleFile: " << iosFile; 99 | 100 | QFileInfo theFile(iosFile); 101 | 102 | if (!theFile.exists()) { 103 | qWarning("iOS File does N O T exist"); 104 | return false; 105 | } 106 | qDebug("Path from QML: The file E X I S T S"); 107 | if (theFile.isReadable()) { 108 | qDebug("iosFile SUCCESS: can open file for reading"); 109 | return true; 110 | } else { 111 | qWarning("iosFile FAILS: can NOT open file for reading"); 112 | } 113 | return false; 114 | #endif 115 | // not used yet for other OS 116 | return false; 117 | } 118 | 119 | void ShareUtils::onShareEditDone(int requestCode) 120 | { 121 | emit shareEditDone(requestCode); 122 | } 123 | 124 | void ShareUtils::onShareFinished(int requestCode) 125 | { 126 | emit shareFinished(requestCode); 127 | } 128 | 129 | void ShareUtils::onShareNoAppAvailable(int requestCode) 130 | { 131 | emit shareNoAppAvailable(requestCode); 132 | } 133 | 134 | void ShareUtils::onShareError(int requestCode, QString message) 135 | { 136 | emit shareError(requestCode, message); 137 | } 138 | 139 | void ShareUtils::onFileUrlReceived(QString url) 140 | { 141 | emit fileUrlReceived(url); 142 | } 143 | 144 | void ShareUtils::onFileReceivedAndSaved(QString url) 145 | { 146 | emit fileReceivedAndSaved(url); 147 | } 148 | 149 | -------------------------------------------------------------------------------- /cpp/shareutils.hpp: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) 2 | // this project is based on ideas from 3 | // http://blog.lasconic.com/share-on-ios-and-android-using-qml/ 4 | // see github project https://github.com/lasconic/ShareUtils-QML 5 | // also inspired by: 6 | // https://www.androidcode.ninja/android-share-intent-example/ 7 | // https://www.calligra.org/blogs/sharing-with-qt-on-android/ 8 | // https://stackoverflow.com/questions/7156932/open-file-in-another-app 9 | // http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData 10 | // see also /COPYRIGHT and /LICENSE 11 | 12 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 13 | // my blog about Qt for mobile: http://j.mp/qt-x 14 | // see also /COPYRIGHT and /LICENSE 15 | 16 | #ifndef SHAREUTILS_H 17 | #define SHAREUTILS_H 18 | 19 | #include 20 | 21 | #include 22 | 23 | class PlatformShareUtils : public QObject 24 | { 25 | Q_OBJECT 26 | signals: 27 | void shareEditDone(int requestCode); 28 | void shareFinished(int requestCode); 29 | void shareNoAppAvailable(int requestCode); 30 | void shareError(int requestCode, QString message); 31 | void fileUrlReceived(QString url); 32 | void fileReceivedAndSaved(QString url); 33 | 34 | public: 35 | PlatformShareUtils(QObject *parent = 0) : QObject(parent){} 36 | virtual ~PlatformShareUtils() {} 37 | virtual bool checkMimeTypeView(const QString &mimeType){ 38 | qDebug() << "check view for " << mimeType; 39 | return true;} 40 | virtual bool checkMimeTypeEdit(const QString &mimeType){ 41 | qDebug() << "check edit for " << mimeType; 42 | return true;} 43 | virtual void share(const QString &text, const QUrl &url){ qDebug() << text << url; } 44 | virtual void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl){ 45 | qDebug() << filePath << " - " << title << "requestId " << requestId << " - " << mimeType << "altImpl? " << altImpl; } 46 | virtual void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl){ 47 | qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? " << altImpl; } 48 | virtual void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl){ 49 | qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? " << altImpl; } 50 | 51 | virtual void checkPendingIntents(const QString workingDirPath){ 52 | qDebug() << "checkPendingIntents " << workingDirPath; } 53 | }; 54 | 55 | class ShareUtils : public QObject 56 | { 57 | Q_OBJECT 58 | 59 | 60 | signals: 61 | void shareEditDone(int requestCode); 62 | void shareFinished(int requestCode); 63 | void shareNoAppAvailable(int requestCode); 64 | void shareError(int requestCode, QString message); 65 | void fileUrlReceived(QString url); 66 | void fileReceivedAndSaved(QString url); 67 | 68 | public slots: 69 | void onShareEditDone(int requestCode); 70 | void onShareFinished(int requestCode); 71 | void onShareNoAppAvailable(int requestCode); 72 | void onShareError(int requestCode, QString message); 73 | void onFileUrlReceived(QString url); 74 | void onFileReceivedAndSaved(QString url); 75 | 76 | public: 77 | explicit ShareUtils(QObject *parent = 0); 78 | Q_INVOKABLE bool checkMimeTypeView(const QString &mimeType); 79 | Q_INVOKABLE bool checkMimeTypeEdit(const QString &mimeType); 80 | Q_INVOKABLE void share(const QString &text, const QUrl &url); 81 | Q_INVOKABLE void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl); 82 | Q_INVOKABLE void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl); 83 | Q_INVOKABLE void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl); 84 | Q_INVOKABLE void checkPendingIntents(const QString workingDirPath); 85 | 86 | // testing native FileDialog 87 | Q_INVOKABLE bool verifyFileUrl(const QString &fileUrl); 88 | 89 | private: 90 | PlatformShareUtils* mPlatformShareUtils; 91 | 92 | }; 93 | 94 | #endif //SHAREUTILS_H 95 | -------------------------------------------------------------------------------- /data_assets.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | data_assets/qt-logo.png 4 | data_assets/share_file.pdf 5 | data_assets/test.docx 6 | data_assets/crete.jpg 7 | 8 | 9 | -------------------------------------------------------------------------------- /data_assets/crete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/data_assets/crete.jpg -------------------------------------------------------------------------------- /data_assets/qt-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/data_assets/qt-logo.png -------------------------------------------------------------------------------- /data_assets/share_file.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/data_assets/share_file.pdf -------------------------------------------------------------------------------- /data_assets/test.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/data_assets/test.docx -------------------------------------------------------------------------------- /deployment.pri: -------------------------------------------------------------------------------- 1 | unix:!android { 2 | isEmpty(target.path) { 3 | qnx { 4 | target.path = /tmp/$${TARGET}/bin 5 | } else { 6 | target.path = /opt/$${TARGET}/bin 7 | } 8 | export(target.path) 9 | } 10 | INSTALLS += target 11 | } 12 | 13 | export(INSTALLS) 14 | -------------------------------------------------------------------------------- /docs/android_share_chooser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/android_share_chooser.png -------------------------------------------------------------------------------- /docs/android_share_send_chooser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/android_share_send_chooser.png -------------------------------------------------------------------------------- /docs/file_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/file_flow.png -------------------------------------------------------------------------------- /docs/handle_url_from_ios_apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/handle_url_from_ios_apps.png -------------------------------------------------------------------------------- /docs/ios_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/ios_preview.png -------------------------------------------------------------------------------- /docs/ios_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/ios_share.png -------------------------------------------------------------------------------- /docs/new_intent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/new_intent.png -------------------------------------------------------------------------------- /docs/process_intent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/process_intent.png -------------------------------------------------------------------------------- /docs/qt_blog_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/qt_blog_overview.png -------------------------------------------------------------------------------- /docs/share_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/share_overview.png -------------------------------------------------------------------------------- /docs/share_overview_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekke/ekkesSHAREexample/293d41f52f726b29a49b89547a32a0287d41e7ef/docs/share_overview_v2.png -------------------------------------------------------------------------------- /ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIconFile 6 | 7 | CFBundlePackageType 8 | APPL 9 | CFBundleGetInfoString 10 | Created by Qt/QMake 11 | CFBundleSignature 12 | ???? 13 | CFBundleExecutable 14 | share_example_x 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleDisplayName 18 | ${PRODUCT_NAME} 19 | CFBundleName 20 | ${PRODUCT_NAME} 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationPortraitUpsideDown 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | NSPhotoLibraryUsageDescription 37 | This App uses Photos 38 | NSCameraUsageDescription 39 | This App needs access to your Camera. 40 | CFBundleDocumentTypes 41 | 42 | 43 | CFBundleTypeName 44 | Generic File 45 | CFBundleTypeRole 46 | Viewer 47 | LSHandlerRank 48 | Alternate 49 | LSItemContentTypes 50 | 51 | public.data 52 | 53 | 54 | 55 | NOTE 56 | This file was generated by Qt/QMake. 57 | CFBundleAllowMixedLocalizations 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ios/src/docviewcontroller.mm: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #import "DocViewController.hpp" 6 | 7 | #include 8 | 9 | @interface DocViewController () 10 | @end 11 | @implementation DocViewController 12 | #pragma mark - 13 | #pragma mark View Life Cycle 14 | - (void)viewDidLoad { 15 | [super viewDidLoad]; 16 | } 17 | #pragma mark - 18 | #pragma mark Document Interaction Controller Delegate Methods 19 | - (UIViewController *) documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller { 20 | #pragma unused (controller) 21 | return self; 22 | } 23 | - (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller 24 | { 25 | #pragma unused (controller) 26 | qDebug() << "end preview"; 27 | 28 | self.mIosShareUtils->handleDocumentPreviewDone(self.requestId); 29 | 30 | [self removeFromParentViewController]; 31 | } 32 | @end 33 | -------------------------------------------------------------------------------- /ios/src/iosshareutils.mm: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | #import "iosshareutils.hpp" 6 | 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | 14 | #import 15 | 16 | #import "docviewcontroller.hpp" 17 | 18 | IosShareUtils::IosShareUtils(QObject *parent) : PlatformShareUtils(parent) 19 | { 20 | // Sharing Files from other iOS Apps I got the ideas and some code contribution from: 21 | // Thomas K. Fischer (@taskfabric) - http://taskfabric.com - thx 22 | QDesktopServices::setUrlHandler("file", this, "handleFileUrlReceived"); 23 | } 24 | 25 | bool IosShareUtils::checkMimeTypeView(const QString &mimeType) { 26 | #pragma unused (mimeType) 27 | // dummi implementation on iOS 28 | // MimeType not used yet 29 | return true; 30 | } 31 | 32 | bool IosShareUtils::checkMimeTypeEdit(const QString &mimeType) { 33 | #pragma unused (mimeType) 34 | // dummi implementation on iOS 35 | // MimeType not used yet 36 | return true; 37 | } 38 | 39 | void IosShareUtils::share(const QString &text, const QUrl &url) { 40 | 41 | NSMutableArray *sharingItems = [NSMutableArray new]; 42 | 43 | if (!text.isEmpty()) { 44 | [sharingItems addObject:text.toNSString()]; 45 | } 46 | if (url.isValid()) { 47 | [sharingItems addObject:url.toNSURL()]; 48 | } 49 | 50 | // get the main window rootViewController 51 | UIViewController *qtUIViewController = [[UIApplication sharedApplication].keyWindow rootViewController]; 52 | 53 | UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil]; 54 | if ( [activityController respondsToSelector:@selector(popoverPresentationController)] ) { // iOS8 55 | activityController.popoverPresentationController.sourceView = qtUIViewController.view; 56 | } 57 | [qtUIViewController presentViewController:activityController animated:YES completion:nil]; 58 | } 59 | 60 | // altImpl not used yet on iOS, on Android twi ways to use JNI 61 | void IosShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) { 62 | #pragma unused (title, mimeType, altImpl) 63 | 64 | NSString* nsFilePath = filePath.toNSString(); 65 | NSURL *nsFileUrl = [NSURL fileURLWithPath:nsFilePath]; 66 | 67 | static DocViewController* docViewController = nil; 68 | if(docViewController!=nil) 69 | { 70 | [docViewController removeFromParentViewController]; 71 | [docViewController release]; 72 | } 73 | 74 | UIDocumentInteractionController* documentInteractionController = nil; 75 | documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:nsFileUrl]; 76 | 77 | UIViewController* qtUIViewController = [[[[UIApplication sharedApplication]windows] firstObject]rootViewController]; 78 | if(qtUIViewController!=nil) 79 | { 80 | docViewController = [[DocViewController alloc] init]; 81 | 82 | docViewController.requestId = requestId; 83 | // we need this to be able to execute handleDocumentPreviewDone() method, 84 | // when preview was finished 85 | docViewController.mIosShareUtils = this; 86 | 87 | [qtUIViewController addChildViewController:docViewController]; 88 | documentInteractionController.delegate = docViewController; 89 | // [documentInteractionController presentPreviewAnimated:YES]; 90 | if(![documentInteractionController presentPreviewAnimated:YES]) 91 | { 92 | emit shareError(0, tr("No App found to open: %1").arg(filePath)); 93 | } 94 | } 95 | } 96 | 97 | 98 | void IosShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) { 99 | #pragma unused (title, mimeType) 100 | 101 | sendFile(filePath, title, mimeType, requestId, altImpl); 102 | } 103 | 104 | void IosShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId, const bool &altImpl) { 105 | #pragma unused (title, mimeType) 106 | 107 | sendFile(filePath, title, mimeType, requestId, altImpl); 108 | } 109 | 110 | void IosShareUtils::handleDocumentPreviewDone(const int &requestId) 111 | { 112 | // documentInteractionControllerDidEndPreview 113 | qDebug() << "handleShareDone: " << requestId; 114 | emit shareFinished(requestId); 115 | } 116 | 117 | void IosShareUtils::handleFileUrlReceived(const QUrl &url) 118 | { 119 | QString incomingUrl = url.toString(); 120 | if(incomingUrl.isEmpty()) { 121 | qWarning() << "setFileUrlReceived: we got an empty URL"; 122 | emit shareError(0, tr("Empty URL received")); 123 | return; 124 | } 125 | qDebug() << "IosShareUtils setFileUrlReceived: we got the File URL from iOS: " << incomingUrl; 126 | QString myUrl; 127 | if(incomingUrl.startsWith("file://")) { 128 | myUrl= incomingUrl.right(incomingUrl.length()-7); 129 | qDebug() << "QFile needs this URL: " << myUrl; 130 | } else { 131 | myUrl= incomingUrl; 132 | } 133 | 134 | // check if File exists 135 | QFileInfo fileInfo = QFileInfo(myUrl); 136 | if(fileInfo.exists()) { 137 | emit fileUrlReceived(myUrl); 138 | } else { 139 | qDebug() << "setFileUrlReceived: FILE does NOT exist "; 140 | emit shareError(0, tr("File does not exist: %1").arg(myUrl)); 141 | } 142 | } 143 | 144 | 145 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | qml/main.qml 4 | 5 | 6 | -------------------------------------------------------------------------------- /qml/main.qml: -------------------------------------------------------------------------------- 1 | // (c) 2017 Ekkehard Gentz (ekke) @ekkescorner 2 | // my blog about Qt for mobile: http://j.mp/qt-x 3 | // see also /COPYRIGHT and /LICENSE 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | import QtQuick.Layouts 1.15 8 | import QtQuick.Controls.Material 2.15 9 | import QtQuick.Dialogs 1.3 10 | 11 | ApplicationWindow { 12 | id: appWindow 13 | visible: true 14 | width: 640 15 | height: 480 16 | title: qsTr("Share Examples") 17 | 18 | // some request ids to test 19 | // in real-life apps you would use customerNumber, orderId, workflowId or similoar values to identify the context when getting a value back 20 | property int request_NO_RESPONSE_IMAGE: 0; 21 | property int request_NO_RESPONSE_PDF: -1; 22 | property int request_NO_RESPONSE_JPEG: -2; 23 | property int request_NO_RESPONSE_DOCX: -3; 24 | 25 | property int request_EDIT_FILE_IMAGE: 42; 26 | property int request_EDIT_FILE_PDF: 44; 27 | property int request_EDIT_FILE_JPEG: 45; 28 | property int request_EDIT_FILE_DOCX: 46; 29 | 30 | property int request_VIEW_FILE_IMAGE: 22; 31 | property int request_VIEW_FILE_PDF: 21; 32 | property int request_VIEW_FILE_JPEG: 23; 33 | property int request_VIEW_FILE_DOCX: 24; 34 | 35 | property int request_SEND_FILE_IMAGE: 11; 36 | property int request_SEND_FILE_PDF: 10; 37 | property int request_SEND_FILE_JPEG: 12; 38 | property int request_SEND_FILE_DOCX: 13; 39 | 40 | property int index_PNG: 0 41 | property int index_JPEG: 1 42 | property int index_DOCX: 2 43 | property int index_PDF: 3 44 | 45 | property var theModel: ["Image (PNG)","Image (JPEG)", "Document (DOCX)", "PDF"] 46 | 47 | property bool useAltImpl: false // now always false 48 | 49 | 50 | SwipeView { 51 | id: swipeView 52 | anchors.fill: parent 53 | currentIndex: tabBar.currentIndex 54 | 55 | Page { 56 | id: homePage 57 | Image { 58 | id: image0 59 | anchors.top: parent.top 60 | anchors.right: parent.right 61 | sourceSize.width: 160 62 | MouseArea { 63 | anchors.fill: parent 64 | onClicked: { 65 | image0.source = "" 66 | } 67 | } 68 | } 69 | Label { 70 | id: titleLabel 71 | text: qsTr("Welcome to ekke's Share Example App") 72 | wrapMode: Label.WordWrap 73 | anchors.top: image0.bottom 74 | anchors.left: parent.left 75 | anchors.topMargin: 24 76 | anchors.leftMargin: 24 77 | anchors.right: parent.right 78 | anchors.rightMargin: 24 79 | } 80 | Label { 81 | id: infoLabel 82 | text: qsTr("Swipe through Pages or TabBar to Share Text or to Send / View / Edit File (PNG, JPEG, TXT, DOCX, PDF)") 83 | wrapMode: Label.WordWrap 84 | anchors.top: titleLabel.bottom 85 | anchors.left: parent.left 86 | anchors.topMargin: 24 87 | anchors.leftMargin: 24 88 | anchors.right: parent.right 89 | anchors.rightMargin: 24 90 | } 91 | Label { 92 | id: reverseLabel 93 | text: qsTr("Sharing the reverse Way from other Apps to our App:\nGoTo the Page where you want to get the file.\nSwitch to another App, share File with our Example App.\nSingle Image should appear on current Page, other Filetypes or more Files should open a Popup") 94 | wrapMode: Label.WordWrap 95 | anchors.top: infoLabel.bottom // Qt.platform.os === "android"? androidInfoLabel.bottom : infoLabel.bottom 96 | anchors.left: parent.left 97 | anchors.topMargin: 24 98 | anchors.leftMargin: 24 99 | anchors.right: parent.right 100 | anchors.rightMargin: 24 101 | } 102 | Label { 103 | id: permissionLabel 104 | visible: Qt.platform.os === "android" 105 | text: qsTr("To receive Files from other Apps you need WRITE_EXTERNAL_STORAGE Permission. This App will ask you if Permission not set yet.\n") 106 | wrapMode: Label.WordWrap 107 | anchors.top: reverseLabel.bottom 108 | anchors.left: parent.left 109 | anchors.leftMargin: 24 110 | anchors.right: parent.right 111 | anchors.rightMargin: 24 112 | } 113 | Label { 114 | id: dialogLabel 115 | text: qsTr("Instead of calling our App from other Apps to get files, we can also use the native FileDialog from QML.\nExample implemented at VIEW Page.\n") 116 | color: Material.primaryColor 117 | wrapMode: Label.WordWrap 118 | anchors.top: permissionLabel.bottom 119 | anchors.left: parent.left 120 | anchors.leftMargin: 24 121 | anchors.right: parent.right 122 | anchors.rightMargin: 24 123 | } 124 | Label { 125 | id: manageFilesLabel 126 | visible: Qt.platform.os === "android" 127 | text: qsTr("The App also includes an example HowTo grant access to MANAGE_EXTERNAL_STORAGE.\nsee this main.qml Component.onClompeted\n") 128 | color: Material.accentColor 129 | wrapMode: Label.WordWrap 130 | anchors.top: dialogLabel.bottom 131 | anchors.left: parent.left 132 | anchors.leftMargin: 24 133 | anchors.right: parent.right 134 | anchors.rightMargin: 24 135 | } 136 | } 137 | 138 | Page { 139 | id: pageTextUrl 140 | Button { 141 | id: shareButton 142 | text: qsTr("Share Text and Url") 143 | anchors.top: parent.top 144 | anchors.left: parent.left 145 | anchors.leftMargin: 24 146 | anchors.topMargin: 24 147 | onClicked: { 148 | shareUtils.share("Qt","http://qt.io") 149 | } 150 | } 151 | Image { 152 | id: image1 153 | anchors.top: parent.top 154 | anchors.right: parent.right 155 | sourceSize.width: 160 156 | MouseArea { 157 | anchors.fill: parent 158 | onClicked: { 159 | image1.source = "" 160 | } 161 | } 162 | } 163 | } 164 | 165 | Page { 166 | id: pageSend 167 | ComboBox { 168 | id: sendSwitch 169 | model: theModel 170 | currentIndex: 0 171 | anchors.top: parent.top // Qt.platform.os === "android"? sendJNISwitch.bottom : parent.top 172 | anchors.left: parent.left 173 | anchors.leftMargin: 24 174 | anchors.topMargin: appWindow.useAltImpl? 32 : 12 175 | anchors.right: sendButtonWResult.right 176 | } 177 | Button { 178 | id: sendButton 179 | text: Qt.platform.os === "android"? qsTr("Send File\n(no feedback)") : qsTr("Send File") 180 | onClicked: { 181 | if(sendSwitch.currentIndex === index_PNG) { 182 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_IMAGE), "Send File", "image/png", request_NO_RESPONSE_IMAGE, appWindow.useAltImpl) 183 | return 184 | } 185 | if(sendSwitch.currentIndex === index_JPEG) { 186 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_JPEG), "Send File", "image/jpeg", request_NO_RESPONSE_JPEG, appWindow.useAltImpl) 187 | return 188 | } 189 | if(sendSwitch.currentIndex === index_DOCX) { 190 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_DOCX), "Send File", "", request_NO_RESPONSE_DOCX, appWindow.useAltImpl) 191 | return 192 | } 193 | if(sendSwitch.currentIndex === index_PDF) { 194 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_PDF), "Send File", "application/pdf", request_NO_RESPONSE_PDF, appWindow.useAltImpl) 195 | return 196 | } 197 | } 198 | anchors.top: sendSwitch.bottom 199 | anchors.left: parent.left 200 | anchors.leftMargin: 24 201 | anchors.topMargin: 24 202 | } 203 | Button { 204 | id: sendButtonWResult 205 | text: Qt.platform.os === "android"? qsTr("Send File with Result\n(recommended)") : qsTr("Send File with RequestId\n(recommended)") 206 | onClicked: { 207 | if(sendSwitch.currentIndex === index_PNG) { 208 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_SEND_FILE_IMAGE), "Send File", "image/png", request_SEND_FILE_IMAGE, appWindow.useAltImpl) 209 | return 210 | } 211 | if(sendSwitch.currentIndex === index_JPEG) { 212 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_SEND_FILE_JPEG), "Send File", "image/jpeg", request_SEND_FILE_JPEG, appWindow.useAltImpl) 213 | return 214 | } 215 | if(sendSwitch.currentIndex === index_DOCX) { 216 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_SEND_FILE_DOCX), "Send File", "", request_SEND_FILE_DOCX, appWindow.useAltImpl) 217 | return 218 | } 219 | if(sendSwitch.currentIndex === index_PDF) { 220 | shareUtils.sendFile(copyFileFromAppDataIntoDocuments(request_SEND_FILE_PDF), "Send File", "application/pdf", request_SEND_FILE_PDF, appWindow.useAltImpl) 221 | return 222 | } 223 | } 224 | anchors.top: sendButton.bottom 225 | anchors.left: parent.left 226 | anchors.leftMargin: 24 227 | anchors.topMargin: 24 228 | } 229 | Image { 230 | id: image2 231 | anchors.top: parent.top 232 | anchors.right: parent.right 233 | sourceSize.width: 160 234 | MouseArea { 235 | anchors.fill: parent 236 | onClicked: { 237 | image2.source = "" 238 | } 239 | } 240 | } 241 | } 242 | 243 | Page { 244 | id: pageView 245 | ComboBox { 246 | id: viewSwitch 247 | model: theModel 248 | currentIndex: 0 249 | anchors.top: parent.top // Qt.platform.os === "android"? viewJNISwitch.bottom : parent.top 250 | anchors.left: parent.left 251 | anchors.leftMargin: 24 252 | anchors.topMargin: appWindow.useAltImpl? 32 : 12 253 | anchors.right: viewButtonWResult.right 254 | } 255 | Button { 256 | id: viewButton 257 | text: Qt.platform.os === "android"? qsTr("View File\n(no feedback)") : qsTr("View File") 258 | onClicked: { 259 | if(viewSwitch.currentIndex === index_PNG) { 260 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_IMAGE), "Send File", "image/png", request_NO_RESPONSE_IMAGE, appWindow.useAltImpl) 261 | return 262 | } 263 | if(viewSwitch.currentIndex === index_JPEG) { 264 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_JPEG), "Send File", "image/jpeg", request_NO_RESPONSE_JPEG, appWindow.useAltImpl) 265 | return 266 | } 267 | if(viewSwitch.currentIndex === index_DOCX) { 268 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_DOCX), "Send File", "", request_NO_RESPONSE_DOCX, appWindow.useAltImpl) 269 | return 270 | } 271 | if(viewSwitch.currentIndex === index_PDF) { 272 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_PDF), "Send File", "application/pdf", request_NO_RESPONSE_PDF, appWindow.useAltImpl) 273 | return 274 | } 275 | } 276 | anchors.top: viewSwitch.bottom 277 | anchors.left: parent.left 278 | anchors.leftMargin: 24 279 | anchors.topMargin: 24 280 | } 281 | Button { 282 | id: viewButtonWResult 283 | text: Qt.platform.os === "android"? qsTr("View File with Result\n(recommended)") : qsTr("View File with RequestId\n(recommended)") 284 | onClicked: { 285 | if(viewSwitch.currentIndex === index_PNG) { 286 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_VIEW_FILE_IMAGE), "View File", "image/png", request_VIEW_FILE_IMAGE, appWindow.useAltImpl) 287 | return 288 | } 289 | if(viewSwitch.currentIndex === index_JPEG) { 290 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_VIEW_FILE_JPEG), "View File", "image/jpeg", request_VIEW_FILE_JPEG, appWindow.useAltImpl) 291 | return 292 | } 293 | if(viewSwitch.currentIndex === index_DOCX) { 294 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_VIEW_FILE_DOCX), "View File", "", request_VIEW_FILE_DOCX, appWindow.useAltImpl) 295 | return 296 | } 297 | if(viewSwitch.currentIndex === index_PDF) { 298 | shareUtils.viewFile(copyFileFromAppDataIntoDocuments(request_VIEW_FILE_PDF), "View File", "application/pdf", request_VIEW_FILE_PDF, appWindow.useAltImpl) 299 | return 300 | } 301 | } 302 | anchors.top: viewButton.bottom 303 | anchors.left: parent.left 304 | anchors.leftMargin: 24 305 | anchors.topMargin: 24 306 | } 307 | Button { 308 | id: viewButtonCheckMime 309 | text: Qt.platform.os === "android"? qsTr("Check MimeType for VIEW") : qsTr("Check MimeType for VIEW\n(not used yet on iOS)") 310 | property var theMimeTypes: ["image/png","image/jpeg","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/pdf"] 311 | property string mimeType: theMimeTypes[viewSwitch.currentIndex] 312 | onClicked: { 313 | var verified = shareUtils.checkMimeTypeView(mimeType) 314 | if(verified) { 315 | popup.labelText = "success:\nApps available to View\n"+mimeType 316 | popup.open() 317 | } else { 318 | popup.labelText = "sorry:\nNO Apps available to View\n"+mimeType 319 | popup.open() 320 | } 321 | } 322 | anchors.top: viewButtonWResult.bottom 323 | anchors.left: parent.left 324 | anchors.leftMargin: 24 325 | anchors.topMargin: 24 326 | } 327 | Label { 328 | id: fileDialogLabel 329 | text: qsTr("View (read/copy) Files using native FileDialog.\nAttention\nAndroid: watch requestCode 1305 in QShareActivity.java onActivityResult()\nIOS: fileUrl must be converted to local file to be used in CPP") 330 | color: Material.primaryColor 331 | wrapMode: Label.WordWrap 332 | anchors.top: viewButtonCheckMime.bottom 333 | anchors.left: parent.left 334 | anchors.right: parent.right 335 | anchors.rightMargin: 24 336 | anchors.leftMargin: 24 337 | anchors.topMargin: 24 338 | } 339 | FileDialog { 340 | id: myFileDialog 341 | title: qsTr("Select a File") 342 | selectExisting: true 343 | selectFolder: false 344 | // to make it simpler in this example we only deal with the first selected file 345 | // HINT: on IOS there always can only be selected ONE file 346 | // to enable multi files you have to create a FileDialog by yourself === some work to get a great UX 347 | selectMultiple: true 348 | folder: Qt.platform.os === "ios"? shortcuts.pictures:"" 349 | onAccepted: { 350 | if(fileUrls.length) { 351 | console.log("we selected: ", fileUrls[0]) 352 | // hint: on IOS the fileUrl must be converted to local file 353 | // to be understood by CPP see shareUtils.verifyFileUrl 354 | if(shareUtils.verifyFileUrl(fileUrls[0])) { 355 | console.log("YEP: the file exists") 356 | popup.labelText = qsTr("The File exists and can be accessed\n%1").arg(fileUrls[0]) 357 | popup.open() 358 | // it's up to you to copy the file, display image, etc 359 | // here we only check if File can be accessed or not 360 | return 361 | } else { 362 | if(Qt.platform.os === "ios") { 363 | popup.labelText = qsTr("The File CANNOT be accessed.\n%1").arg(fileUrls[0]) 364 | } else { 365 | // Attention with Spaces or Umlauts in FileName if 5.15.14 366 | // see https://bugreports.qt.io/browse/QTBUG-114435 367 | // and workaround for SPACES: https://bugreports.qt.io/browse/QTBUG-112663 368 | popup.labelText = qsTr("The File CANNOT be accessed.\nFile Names with spaces or Umlauts please wait for Qt 5.15.15\n%1").arg(fileUrls[0]) 369 | } 370 | popup.open() 371 | } 372 | } else { 373 | console.log("no file selected") 374 | } 375 | } 376 | } // myFileDialog 377 | Button { 378 | id: fileDialogButton 379 | text: qsTr("Select File from FileDialog") 380 | anchors.top: fileDialogLabel.bottom 381 | anchors.left: parent.left 382 | anchors.leftMargin: 24 383 | anchors.topMargin: 24 384 | onClicked: { 385 | myFileDialog.open() 386 | } 387 | } 388 | 389 | Image { 390 | id: image3 391 | anchors.top: parent.top 392 | anchors.right: parent.right 393 | sourceSize.width: 160 394 | MouseArea { 395 | anchors.fill: parent 396 | onClicked: { 397 | image3.source = "" 398 | } 399 | } 400 | } 401 | } 402 | 403 | Page { 404 | id: pageEdit 405 | ComboBox { 406 | id: editSwitch 407 | model: theModel 408 | currentIndex: 0 409 | anchors.top: parent.top // Qt.platform.os === "android"? editJNISwitch.bottom : parent.top 410 | anchors.left: parent.left 411 | anchors.leftMargin: 24 412 | anchors.topMargin: appWindow.useAltImpl? 32 : 12 413 | anchors.right: editButtonWResult.right 414 | } 415 | Button { 416 | id: editButton 417 | text: Qt.platform.os === "android"? qsTr("Edit File\n(no feedback)") : qsTr("Edit File") 418 | onClicked: { 419 | if(editSwitch.currentIndex === index_PNG) { 420 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_IMAGE), "Edit File", "image/png", request_NO_RESPONSE_IMAGE, appWindow.useAltImpl) 421 | return 422 | } 423 | if(editSwitch.currentIndex === index_JPEG) { 424 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_JPEG), "Edit File", "image/jpeg", request_NO_RESPONSE_JPEG, appWindow.useAltImpl) 425 | return 426 | } 427 | if(editSwitch.currentIndex === index_DOCX) { 428 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_DOCX), "Edit File", "", request_NO_RESPONSE_DOCX, appWindow.useAltImpl) 429 | return 430 | } 431 | if(editSwitch.currentIndex === index_PDF) { 432 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_NO_RESPONSE_PDF), "Edit File", "application/pdf", request_NO_RESPONSE_PDF, appWindow.useAltImpl) 433 | return 434 | } 435 | } 436 | anchors.top: editSwitch.bottom 437 | anchors.left: parent.left 438 | anchors.leftMargin: 24 439 | anchors.topMargin: 24 440 | } 441 | Button { 442 | id: editButtonWResult 443 | text: Qt.platform.os === "android"? qsTr("Edit File with Result\n(recommended)") : qsTr("Edit File with RequestId\n(recommeded)") 444 | onClicked: { 445 | if(editSwitch.currentIndex === index_PNG) { 446 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_EDIT_FILE_IMAGE), "Edit File", "image/png", request_EDIT_FILE_IMAGE, appWindow.useAltImpl) 447 | return 448 | } 449 | if(editSwitch.currentIndex === index_JPEG) { 450 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_EDIT_FILE_JPEG), "Edit File", "image/jpeg", request_EDIT_FILE_JPEG, appWindow.useAltImpl) 451 | return 452 | } 453 | if(editSwitch.currentIndex === index_DOCX) { 454 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_EDIT_FILE_DOCX), "Edit File", "", request_EDIT_FILE_DOCX, appWindow.useAltImpl) 455 | return 456 | } 457 | if(editSwitch.currentIndex === index_PDF) { 458 | shareUtils.editFile(copyFileFromAppDataIntoDocuments(request_EDIT_FILE_PDF), "Edit File", "application/pdf", request_EDIT_FILE_PDF, appWindow.useAltImpl) 459 | return 460 | } 461 | } 462 | anchors.top: editButton.bottom 463 | anchors.left: parent.left 464 | anchors.leftMargin: 24 465 | anchors.topMargin: 24 466 | } 467 | Button { 468 | id: editButtonCheckMime 469 | text: Qt.platform.os === "android"? qsTr("Check MimeType for EDIT") : qsTr("Check MimeType for EDIT\n(not used yet on iOS)") 470 | property var theMimeTypes: ["image/png","image/jpeg","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/pdf"] 471 | property string mimeType: theMimeTypes[editSwitch.currentIndex] 472 | onClicked: { 473 | var verified = shareUtils.checkMimeTypeEdit(mimeType) 474 | if(verified) { 475 | popup.labelText = "success:\nApps available to Edit\n"+mimeType 476 | popup.open() 477 | } else { 478 | popup.labelText = "sorry:\nNO Apps available to Edit\n"+mimeType 479 | popup.open() 480 | } 481 | } 482 | anchors.top: editButtonWResult.bottom 483 | anchors.left: parent.left 484 | anchors.leftMargin: 24 485 | anchors.topMargin: 24 486 | } 487 | Image { 488 | id: image4 489 | anchors.top: parent.top 490 | anchors.right: parent.right 491 | sourceSize.width: 160 492 | MouseArea { 493 | anchors.fill: parent 494 | onClicked: { 495 | image4.source = "" 496 | } 497 | } 498 | } 499 | } 500 | Component.onCompleted: { 501 | // to get access to all files you need a special permission 502 | // add to your Manifest 503 | // then ask user to get full access 504 | // HINT: if you want to deploy your app via Play Store, you have to ask Google to use this permission 505 | // per ex. an app like a FileManager could be valid 506 | // if your business app is running inside a MDM, you don't need to ask Google 507 | if(Qt.platform.os === "android") { 508 | myApp.accessAllFiles() 509 | } 510 | } 511 | } 512 | 513 | footer: TabBar { 514 | id: tabBar 515 | currentIndex: swipeView.currentIndex 516 | width: parent.width-12 517 | Repeater { 518 | id: tabButtonRepeater 519 | model: [qsTr("Home"), qsTr("Text"), qsTr("Send"), qsTr("View"), qsTr("Edit")] 520 | TabButton { 521 | text: modelData 522 | width: tabBar.width/(tabButtonRepeater.model.length) 523 | } 524 | } // tab repeater 525 | } // footer 526 | 527 | function onShareEditDone(requestCode) { 528 | console.log ("share done: "+ requestCode) 529 | if(requestCode === request_EDIT_FILE_PDF || requestCode === request_EDIT_FILE_IMAGE) { 530 | popup.labelText = "Edit Done" 531 | popup.open() 532 | requestEditDone(requestCode) 533 | return 534 | } 535 | popup.labelText = "Done" 536 | popup.open() 537 | } 538 | Timer { 539 | id: delayDeleteTimer 540 | property int theRequestCode 541 | interval: 500 542 | repeat: false 543 | onTriggered: { 544 | requestCanceledOrViewDoneOrSendDone(theRequestCode) 545 | } 546 | } 547 | 548 | function onShareFinished(requestCode) { 549 | console.log ("share canceled: "+ requestCode) 550 | if(requestCode === request_VIEW_FILE_PDF || requestCode === request_VIEW_FILE_IMAGE || requestCode === request_VIEW_FILE_JPEG || requestCode === request_VIEW_FILE_DOCX) { 551 | popup.labelText = "View finished or canceled" 552 | popup.open() 553 | requestCanceledOrViewDoneOrSendDone(requestCode) 554 | return 555 | } 556 | if(requestCode === request_EDIT_FILE_PDF || requestCode === request_EDIT_FILE_IMAGE || requestCode === request_EDIT_FILE_JPEG || requestCode === request_EDIT_FILE_DOCX) { 557 | popup.labelText = "Edit canceled" 558 | popup.open() 559 | requestCanceledOrViewDoneOrSendDone(requestCode) 560 | return 561 | } 562 | // Attention using ACTION_SEND it could happen that the Result comes back too fast 563 | // and immediately deleting the file would cause that target app couldn't finish 564 | // copying or printing the file 565 | // workaround: use a Timer 566 | if(requestCode === request_SEND_FILE_PDF || requestCode === request_SEND_FILE_IMAGE || requestCode === request_SEND_FILE_JPEG || requestCode === request_SEND_FILE_DOCX) { 567 | popup.labelText = "Sending File finished or canceled" 568 | popup.open() 569 | if(appWindow.useAltImpl) { 570 | requestCanceledOrViewDoneOrSendDone(requestCode) 571 | } else { 572 | delayDeleteTimer.theRequestCode = requestCode 573 | delayDeleteTimer.start() 574 | } 575 | return 576 | } 577 | popup.labelText = "canceled" 578 | popup.open() 579 | } 580 | function onShareNoAppAvailable(requestCode) { 581 | console.log ("share no App available: "+ requestCode) 582 | if(requestCode === request_VIEW_FILE_PDF || requestCode === request_VIEW_FILE_IMAGE || requestCode === request_VIEW_FILE_JPEG || requestCode === request_VIEW_FILE_DOCX) { 583 | popup.labelText = "No App found (View File)" 584 | popup.open() 585 | requestCanceledOrViewDoneOrSendDone(requestCode) 586 | return 587 | } 588 | if(requestCode === request_EDIT_FILE_PDF || requestCode === request_EDIT_FILE_IMAGE || requestCode === request_EDIT_FILE_JPEG || requestCode === request_EDIT_FILE_DOCX) { 589 | popup.labelText = "No App found (Edit File)" 590 | popup.open() 591 | requestCanceledOrViewDoneOrSendDone(requestCode) 592 | return 593 | } 594 | if(requestCode === request_SEND_FILE_PDF || requestCode === request_SEND_FILE_IMAGE || requestCode === request_SEND_FILE_JPEG || requestCode === request_SEND_FILE_DOCX) { 595 | popup.labelText = "No App found (Send File)" 596 | popup.open() 597 | requestCanceledOrViewDoneOrSendDone(requestCode) 598 | return 599 | } 600 | popup.labelText = "No App found" 601 | popup.open() 602 | } 603 | function onShareError(requestCode, message) { 604 | console.log ("share error: "+ requestCode + " / " + message) 605 | if(requestCode === request_VIEW_FILE_PDF || requestCode === request_VIEW_FILE_IMAGE || requestCode === request_VIEW_FILE_JPEG || requestCode === request_VIEW_FILE_DOCX) { 606 | popup.labelText = "(View File) " + message 607 | popup.open() 608 | requestCanceledOrViewDoneOrSendDone(requestCode) 609 | return 610 | } 611 | if(requestCode === request_EDIT_FILE_PDF || requestCode === request_EDIT_FILE_IMAGE || requestCode === request_EDIT_FILE_JPEG || requestCode === request_EDIT_FILE_DOCX) { 612 | popup.labelText = "(Edit File) " + message 613 | popup.open() 614 | requestCanceledOrViewDoneOrSendDone(requestCode) 615 | return 616 | } 617 | if(requestCode === request_SEND_FILE_PDF || requestCode === request_SEND_FILE_IMAGE || requestCode === request_SEND_FILE_JPEG || requestCode === request_SEND_FILE_DOCX) { 618 | popup.labelText = "(Send File) " + message 619 | popup.open() 620 | requestCanceledOrViewDoneOrSendDone(requestCode) 621 | return 622 | } 623 | popup.labelText = message 624 | popup.open() 625 | } 626 | 627 | function copyFileFromAppDataIntoDocuments(requestId) { 628 | return myApp.filePathDocumentsLocation(requestId) 629 | } 630 | 631 | // we must delete file from DOCUMENTS 632 | // edit canceled, view done, send done or no matching app found 633 | function requestCanceledOrViewDoneOrSendDone(requestId) { 634 | myApp.deleteFromDocumentsLocation(requestId) 635 | } 636 | // we must copy file back from DOCUMENTS into APP DATA and then delete from DOCUMENTS 637 | function requestEditDone(requestId) { 638 | myApp.updateFileFromDocumentsLocation(requestId) 639 | } 640 | 641 | function onNoDocumentsWorkLocation() { 642 | popup.labelText = qsTr("Cannot access external folders and files without checked permissions") 643 | popup.open() 644 | } 645 | 646 | // simulates that you selected a destination directory where the File should be displayed / uploaded, ... 647 | function onFileUrlReceived(url) { 648 | console.log("QML: onFileUrlReceived "+url) 649 | var isImage = false 650 | if(url.endsWith("png") || url.endsWith("jpg") || url.endsWith("jpeg")) { 651 | isImage = true 652 | } 653 | if(!isImage) { 654 | popup.labelText = qsTr("received File is not an Image\n%1").arg(url) 655 | popup.open() 656 | return 657 | } 658 | 659 | if(Qt.platform.os === "android") { 660 | if(!myApp.checkPermission()) { 661 | popup.labelText = qsTr("Displaying the Image needs permission for external storage\n%1").arg(url) 662 | popup.open() 663 | return 664 | } 665 | } 666 | 667 | if(swipeView.currentIndex === 0) { 668 | image0.source = "file://"+url 669 | return 670 | } 671 | if(swipeView.currentIndex === 1) { 672 | image1.source = "file://"+url 673 | return 674 | } 675 | if(swipeView.currentIndex === 2) { 676 | image2.source = "file://"+url 677 | return 678 | } 679 | if(swipeView.currentIndex === 3) { 680 | image3.source = "file://"+url 681 | return 682 | } 683 | if(swipeView.currentIndex === 4) { 684 | image4.source = "file://"+url 685 | return 686 | } 687 | 688 | } 689 | 690 | function onFileReceivedAndSaved(url) { 691 | onFileUrlReceived(url) 692 | } 693 | 694 | Popup { 695 | id: popup 696 | closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape 697 | x: 16 698 | y: 16 699 | implicitHeight: 240 700 | implicitWidth: appWindow.width * .9 701 | property alias labelText: popupLabel.text 702 | Column { 703 | anchors.right: parent.right 704 | anchors.left: parent.left 705 | spacing: 20 706 | Label { 707 | id: popupLabel 708 | topPadding: 8 709 | leftPadding: 8 710 | rightPadding: 8 711 | width: parent.width 712 | text: qsTr("Cannot copy to Documents work folder\nPlease check permissions\nThen restart the App") 713 | wrapMode: Text.WrapAtWordBoundaryOrAnywhere 714 | } 715 | Button { 716 | id: okButton 717 | text: "OK" 718 | onClicked: { 719 | popup.close() 720 | } 721 | } // okButton 722 | } // row button 723 | } // popup 724 | 725 | Connections { 726 | target: shareUtils 727 | onShareEditDone: appWindow.onShareEditDone(requestCode) 728 | } 729 | Connections { 730 | target: shareUtils 731 | onShareFinished: appWindow.onShareFinished(requestCode) 732 | } 733 | Connections { 734 | target: shareUtils 735 | onShareNoAppAvailable: appWindow.onShareNoAppAvailable(requestCode) 736 | } 737 | Connections { 738 | target: shareUtils 739 | onShareError: appWindow.onShareError(requestCode, message) 740 | } 741 | 742 | // noDocumentsWorkLocation 743 | Connections { 744 | target: myApp 745 | onNoDocumentsWorkLocation: appWindow.onNoDocumentsWorkLocation() 746 | } 747 | 748 | // called from outside 749 | Connections { 750 | target: shareUtils 751 | onFileUrlReceived: appWindow.onFileUrlReceived(url) 752 | } 753 | 754 | Connections { 755 | target: shareUtils 756 | onFileReceivedAndSaved: appWindow.onFileReceivedAndSaved(url) 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /share_example_x.pro: -------------------------------------------------------------------------------- 1 | # ekke (Ekkehard Gentz) @ekkescorner 2 | TEMPLATE = app 3 | TARGET = share_example_x 4 | 5 | QT += qml quick quickcontrols2 6 | 7 | CONFIG += c++11 8 | 9 | HEADERS += cpp/shareutils.hpp \ 10 | cpp/applicationui.hpp 11 | 12 | SOURCES += cpp/main.cpp \ 13 | cpp/shareutils.cpp \ 14 | cpp/applicationui.cpp 15 | 16 | OTHER_FILES += qml/main.qml 17 | 18 | OTHER_FILES += data_assets/*.png \ 19 | data_assets/*.pdf \ 20 | translations/*.* \ 21 | *.md \ 22 | ios/*.png \ 23 | docs/*.png \ 24 | LICENSE \ 25 | COPYRIGHT 26 | 27 | # can be placed under ios only, but I prefer to see them always 28 | OTHER_FILES += ios/src/*.mm 29 | 30 | # can be placed under android only, but I prefer to see them always 31 | OTHER_FILES += android/src/org/ekkescorner/utils/QShareUtils.java \ 32 | android/src/org/ekkescorner/examples/sharex/QShareActivity.java \ 33 | android/src/org/ekkescorner/utils/QSharePathResolver.java 34 | 35 | RESOURCES += qml.qrc \ 36 | data_assets.qrc 37 | 38 | # Additional import path used to resolve QML modules in Qt Creator's code model 39 | QML_IMPORT_PATH = 40 | 41 | # The following define makes your compiler emit warnings if you use 42 | # any feature of Qt which as been marked deprecated (the exact warnings 43 | # depend on your compiler). Please consult the documentation of the 44 | # deprecated API in order to know how to port your code away from it. 45 | DEFINES += QT_DEPRECATED_WARNINGS 46 | 47 | # Default rules for deployment. 48 | include(deployment.pri) 49 | 50 | DISTFILES += \ 51 | android/AndroidManifest.xml \ 52 | android/gradle/wrapper/gradle-wrapper.jar \ 53 | android/gradlew \ 54 | android/res/values/libs.xml \ 55 | android/res/xml/filepaths.xml \ 56 | android/build.gradle \ 57 | android/gradle.properties \ 58 | android/gradle/wrapper/gradle-wrapper.properties \ 59 | android/gradlew.bat \ 60 | data_assets/ekke.jpg 61 | 62 | android { 63 | QT += androidextras 64 | SOURCES += cpp/android/androidshareutils.cpp 65 | HEADERS += cpp/android/androidshareutils.hpp 66 | ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android 67 | # deploying 32-bit and 64-bit APKs you need different VersionCode 68 | # here's my way to solve this - per ex. Version 1.2.3 69 | # aabcddeef aa: 21 (MY_MIN_API), b: 0 (32 Bit) or 1 (64 Bit) c: 0 (unused) 70 | # dd: 01 (Major Release), ee: 02 (Minor Release), f: 3 (Patch Release) 71 | # VersionName 1.2.3 72 | # VersionCode 32 Bit: 210001023 73 | # VersionCode 64 Bit: 211001023 74 | # Version App Bundles: 212001023 75 | defineReplace(droidVersionCode) { 76 | segments = $$split(1, ".") 77 | for (segment, segments): vCode = "$$first(vCode)$$format_number($$segment, width=2 zeropad)" 78 | equals(ANDROID_ABIS, arm64-v8a): \ 79 | prefix = 1 80 | else: equals(ANDROID_ABIS, armeabi-v7a): \ 81 | prefix = 0 82 | else: prefix = 2 83 | # add more cases as needed 84 | return($$first(prefix)0$$first(vCode)) 85 | } 86 | MY_VERSION = 1.2 87 | MY_PATCH_VERSION = 0 88 | MY_MIN_API = 21 89 | ANDROID_VERSION_NAME = $$MY_VERSION"."$$MY_PATCH_VERSION 90 | ANDROID_VERSION_CODE = $$MY_MIN_API$$droidVersionCode($$MY_VERSION)$$MY_PATCH_VERSION 91 | 92 | # find this in shadow build android-build gradle.properties 93 | ANDROID_MIN_SDK_VERSION = "21" 94 | ANDROID_TARGET_SDK_VERSION = "31" 95 | } 96 | 97 | ios { 98 | LIBS += -framework Photos 99 | 100 | OBJECTIVE_SOURCES += ios/src/iosshareutils.mm \ 101 | ios/src/docviewcontroller.mm 102 | 103 | HEADERS += cpp/ios/iosshareutils.hpp \ 104 | cpp/ios/docviewcontroller.hpp 105 | 106 | QMAKE_INFO_PLIST = ios/Info.plist 107 | 108 | QMAKE_IOS_DEPLOYMENT_TARGET = 12.0 109 | 110 | disable_warning.name = GCC_WARN_64_TO_32_BIT_CONVERSION 111 | disable_warning.value = NO 112 | QMAKE_MAC_XCODE_SETTINGS += disable_warning 113 | 114 | # don't need this anymore 115 | # now QtCreator can set iOS development team from iOS Build Settings 116 | # include(ios_signature.pri) 117 | 118 | MY_BUNDLE_ID.name = PRODUCT_BUNDLE_IDENTIFIER 119 | MY_BUNDLE_ID.value = org.ekkescorner.share_example_x 120 | QMAKE_MAC_XCODE_SETTINGS += MY_BUNDLE_ID 121 | 122 | # Note for devices: 1=iPhone, 2=iPad, 1,2=Universal. 123 | QMAKE_APPLE_TARGETED_DEVICE_FAMILY = 1,2 124 | } 125 | --------------------------------------------------------------------------------