├── .gitignore ├── AndroidFileDialog.qml ├── Demo ├── .gitignore ├── ContentTab.qml ├── Demo.pro ├── PreferenceTab.qml ├── UtilsTab.qml ├── android │ ├── AndroidManifest.xml │ ├── build.gradle │ └── res │ │ └── values │ │ └── libs.xml ├── main.cpp ├── main.qml ├── opener.cpp ├── opener.h ├── prefmodel.cpp ├── prefmodel.h ├── qml.qrc └── qtquickcontrols2.conf ├── FileDialog.qml ├── LICENSE ├── README.md ├── android ├── androidutils.gradle └── src │ └── de │ └── skycoder42 │ └── androidutils │ ├── AlarmReceiver.java │ ├── AndroidUtils.java │ ├── FileChooser.java │ └── PrefHelper.java ├── androidutils.cpp ├── androidutils.h ├── androidutils_de.ts ├── androidutils_jni.cpp ├── androidutils_template.ts ├── contentdevice.cpp ├── contentdevice.h ├── de_skycoder42_androidutils.prc ├── de_skycoder42_androidutils.pri ├── de_skycoder42_androidutils.qrc ├── filechooser.cpp ├── filechooser.h ├── qmldir ├── qpm.json ├── qpmx.json ├── sharedpreferences.cpp └── sharedpreferences.h /.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 | # QtCtreator CMake 37 | CMakeLists.txt.user 38 | 39 | # qpm 40 | vendor -------------------------------------------------------------------------------- /AndroidFileDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.8 2 | import QtQuick.Window 2.2 3 | import Qt.labs.platform 1.0 4 | import de.skycoder42.androidutils 1.1 5 | 6 | FileChooser { 7 | id: fileDialog 8 | 9 | property var labsDialog: null 10 | property alias fileChooser: fileDialog 11 | 12 | property bool saveFile: false 13 | property int dialogFlags: 0 14 | property string defaultSuffix 15 | property string folder 16 | property alias file: fileDialog.contentUrl 17 | property var nameFilters 18 | 19 | type: saveFile ? FileChooser.CreateDocument : FileChooser.OpenDocument 20 | contentUrl: folder + "file" + defaultSuffix 21 | } 22 | -------------------------------------------------------------------------------- /Demo/.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | 74 | -------------------------------------------------------------------------------- /Demo/ContentTab.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.8 2 | import QtQuick.Controls 2.1 3 | import QtQuick.Layouts 1.3 4 | import de.skycoder42.androidutils 1.1 5 | import de.skycoder42.androidutils.demo 1.0 6 | 7 | Pane { 8 | FileDialog { 9 | id: fileDialog 10 | 11 | saveFile: openBox.checked 12 | 13 | onAccepted: AndroidUtils.showToast("File choosen: " + fileDialog.file, true) 14 | onRejected: AndroidUtils.showToast("file choosing aborted!") 15 | } 16 | 17 | Opener { 18 | id: opener 19 | 20 | onDataLoaded: editField.text = data 21 | } 22 | 23 | GridLayout { 24 | anchors.fill: parent 25 | columns: 3 26 | 27 | TextField { 28 | id: contentField 29 | text: fileDialog.file 30 | 31 | Layout.fillWidth: true 32 | Layout.columnSpan: 2 33 | 34 | onEditingFinished: chooser.contentUrl = contentField.text 35 | } 36 | 37 | CheckBox { 38 | id: openBox 39 | text: "Get save file" 40 | } 41 | 42 | Button { 43 | id: chooserButton 44 | Layout.fillWidth: true 45 | text: "Open file chooser" 46 | onClicked: fileDialog.open() 47 | } 48 | 49 | Button { 50 | id: editButton 51 | Layout.fillWidth: true 52 | text: "Display/Edit File" 53 | 54 | onClicked: opener.openFile(fileDialog.file) 55 | } 56 | 57 | Button { 58 | id: saveButton 59 | Layout.fillWidth: true 60 | text: "Save File" 61 | 62 | onClicked: opener.saveFile(fileDialog.file, editField.text) 63 | } 64 | 65 | TextArea { 66 | id: editField 67 | Layout.fillHeight: true 68 | Layout.fillWidth: true 69 | Layout.columnSpan: 3 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Demo/Demo.pro: -------------------------------------------------------------------------------- 1 | QT += qml quick widgets 2 | 3 | CONFIG += c++11 4 | 5 | TARGET = AndroidUtilsDemo 6 | 7 | include(../de_skycoder42_androidutils.pri) 8 | 9 | SOURCES += main.cpp \ 10 | opener.cpp \ 11 | prefmodel.cpp 12 | 13 | RESOURCES += qml.qrc 14 | 15 | # Additional import path used to resolve QML modules in Qt Creator's code model 16 | QML_IMPORT_PATH = 17 | 18 | # Additional import path used to resolve QML modules just for Qt Quick Designer 19 | QML_DESIGNER_IMPORT_PATH = 20 | 21 | # The following define makes your compiler emit warnings if you use 22 | # any feature of Qt which as been marked deprecated (the exact warnings 23 | # depend on your compiler). Please consult the documentation of the 24 | # deprecated API in order to know how to port your code away from it. 25 | DEFINES += QT_DEPRECATED_WARNINGS 26 | 27 | # You can also make your code fail to compile if you use deprecated APIs. 28 | # In order to do so, uncomment the following line. 29 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 30 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 31 | 32 | # Default rules for deployment. 33 | qnx: target.path = /tmp/$${TARGET}/bin 34 | else: unix:!android: target.path = /opt/$${TARGET}/bin 35 | !isEmpty(target.path): INSTALLS += target 36 | 37 | DISTFILES += \ 38 | android/AndroidManifest.xml \ 39 | android/res/values/libs.xml \ 40 | android/build.gradle 41 | 42 | ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android 43 | 44 | HEADERS += \ 45 | opener.h \ 46 | prefmodel.h 47 | 48 | !ReleaseBuild:!DebugBuild:!system(qpmx -d $$shell_quote($$_PRO_FILE_PWD_/..) --qmake-run init $$QPMX_EXTRA_OPTIONS $$shell_quote($$QMAKE_QMAKE) $$shell_quote($$OUT_PWD)): error(qpmx initialization failed. Check the compilation log for details.) 49 | else: include($$OUT_PWD/qpmx_generated.pri) 50 | -------------------------------------------------------------------------------- /Demo/PreferenceTab.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.8 2 | import QtQuick.Controls 2.1 3 | import QtQuick.Layouts 1.3 4 | import de.skycoder42.androidutils.demo 1.0 5 | 6 | Pane { 7 | 8 | PrefModel { 9 | id: pModel 10 | } 11 | 12 | GridLayout { 13 | anchors.fill: parent 14 | columns: 3 15 | 16 | TextField { 17 | id: keyField 18 | placeholderText: "key" 19 | } 20 | 21 | TextField { 22 | id: valueField 23 | placeholderText: "value" 24 | Layout.fillWidth: true 25 | } 26 | 27 | ToolButton { 28 | text: "+" 29 | onClicked: pModel.save(keyField.text, valueField.text) 30 | } 31 | 32 | ListView { 33 | model: pModel.model 34 | 35 | Layout.columnSpan: 3 36 | Layout.fillWidth: true 37 | Layout.fillHeight: true 38 | 39 | delegate: ItemDelegate { 40 | text: modelData 41 | width: parent.width 42 | 43 | onPressAndHold: pModel.remove(index) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Demo/UtilsTab.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.8 2 | import QtQuick.Controls 2.1 3 | import QtQuick.Layouts 1.3 4 | import de.skycoder42.androidutils 1.1 5 | 6 | Pane { 7 | ColumnLayout { 8 | anchors.centerIn: parent 9 | 10 | TextField { 11 | id: color 12 | placeholderText: "Enter a hex color value" 13 | inputMask: "\\#HHHHHH" 14 | text: "#ABCDEF" 15 | } 16 | Button { 17 | id: changeButton 18 | text: "Change color" 19 | onClicked: AndroidUtils.setStatusBarColor(color.text) 20 | } 21 | 22 | CheckBox { 23 | id: longBox 24 | text: "Long toast" 25 | } 26 | Button { 27 | id: toastButton 28 | text: "Show Toast" 29 | onClicked: AndroidUtils.showToast("This is a toast", longBox.checked) 30 | } 31 | 32 | Button { 33 | id: longButton 34 | text: "Press me long!" 35 | onPressAndHold: AndroidUtils.hapticFeedback(AndroidUtils.LongPress) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo/android/AndroidManifest.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 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Demo/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.1.0' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | } 15 | } 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | dependencies { 20 | compile fileTree(dir: 'libs', include: ['*.jar']) 21 | } 22 | 23 | android { 24 | /******************************************************* 25 | * The following variables: 26 | * - androidBuildToolsVersion, 27 | * - androidCompileSdkVersion 28 | * - qt5AndroidDir - holds the path to qt android files 29 | * needed to build any Qt application 30 | * on Android. 31 | * 32 | * are defined in gradle.properties file. This file is 33 | * updated by QtCreator and androiddeployqt tools. 34 | * Changing them manually might break the compilation! 35 | *******************************************************/ 36 | 37 | compileSdkVersion androidCompileSdkVersion.toInteger() 38 | 39 | buildToolsVersion androidBuildToolsVersion 40 | 41 | sourceSets { 42 | main { 43 | manifest.srcFile 'AndroidManifest.xml' 44 | java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] 45 | aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] 46 | res.srcDirs = [qt5AndroidDir + '/res', 'res'] 47 | resources.srcDirs = ['src'] 48 | renderscript.srcDirs = ['src'] 49 | assets.srcDirs = ['assets'] 50 | jniLibs.srcDirs = ['libs'] 51 | } 52 | } 53 | 54 | lintOptions { 55 | abortOnError false 56 | } 57 | } 58 | 59 | apply from: "androidutils.gradle" 60 | -------------------------------------------------------------------------------- /Demo/android/res/values/libs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://download.qt.io/ministro/android/qt5/qt-5.8 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "opener.h" 6 | #include "prefmodel.h" 7 | #ifdef Q_OS_ANDROID 8 | #include 9 | #include 10 | #endif 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 15 | QApplication app(argc, argv); 16 | 17 | qmlRegisterType("de.skycoder42.androidutils.demo", 1, 0, "Opener"); 18 | qmlRegisterType("de.skycoder42.androidutils.demo", 1, 0, "PrefModel"); 19 | 20 | QQmlApplicationEngine engine; 21 | engine.load(QUrl(QLatin1String("qrc:/main.qml"))); 22 | 23 | return app.exec(); 24 | } 25 | -------------------------------------------------------------------------------- /Demo/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.8 2 | import QtQuick.Controls 2.1 3 | import QtQuick.Layouts 1.3 4 | import de.skycoder42.androidutils 1.1 5 | import Qt.labs.platform 1.0 as Labs//fix because of file tree 6 | 7 | ApplicationWindow { 8 | visible: true 9 | width: 640 10 | height: 480 11 | title: qsTr("Hello World") 12 | 13 | footer: TabBar { 14 | id: tabBar 15 | currentIndex: swipeView.currentIndex 16 | 17 | TabButton { 18 | id: btn0 19 | text: "Utils" 20 | } 21 | 22 | TabButton { 23 | id: btn1 24 | text: "File Chooser" 25 | } 26 | 27 | TabButton { 28 | id: btn2 29 | text: "Preferences" 30 | } 31 | } 32 | 33 | Page { 34 | anchors.fill: parent 35 | 36 | SwipeView { 37 | id: swipeView 38 | anchors.fill: parent 39 | currentIndex: tabBar.currentIndex 40 | 41 | UtilsTab {} 42 | ContentTab {} 43 | PreferenceTab {} 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Demo/opener.cpp: -------------------------------------------------------------------------------- 1 | #include "opener.h" 2 | #include 3 | #ifdef Q_OS_ANDROID 4 | #include 5 | #include 6 | #endif 7 | 8 | Opener::Opener(QObject *parent) : 9 | QObject(parent) 10 | {} 11 | 12 | void Opener::openFile(const QUrl &file) 13 | { 14 | #ifdef Q_OS_ANDROID 15 | ContentDevice device(QtAndroid::androidActivity(), file); 16 | #else 17 | QFile device(file.toLocalFile()); 18 | #endif 19 | device.open(QIODevice::ReadOnly); 20 | auto data = QString::fromUtf8(device.readAll()); 21 | device.close(); 22 | emit dataLoaded(data); 23 | } 24 | 25 | void Opener::saveFile(const QUrl &file, const QString &data) 26 | { 27 | #ifdef Q_OS_ANDROID 28 | ContentDevice device(QtAndroid::androidActivity(), file); 29 | #else 30 | QFile device(file.toLocalFile()); 31 | #endif 32 | device.open(QIODevice::WriteOnly); 33 | device.write(data.toUtf8()); 34 | device.close(); 35 | } 36 | -------------------------------------------------------------------------------- /Demo/opener.h: -------------------------------------------------------------------------------- 1 | #ifndef OPENER_H 2 | #define OPENER_H 3 | 4 | #include 5 | #include 6 | 7 | class Opener : public QObject 8 | { 9 | Q_OBJECT 10 | public: 11 | explicit Opener(QObject *parent = nullptr); 12 | 13 | public slots: 14 | void openFile(const QUrl &file); 15 | void saveFile(const QUrl &file, const QString &data); 16 | 17 | signals: 18 | void dataLoaded(const QString &data); 19 | }; 20 | 21 | #endif // OPENER_H 22 | -------------------------------------------------------------------------------- /Demo/prefmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "prefmodel.h" 2 | #include 3 | 4 | PrefModel::PrefModel(QObject *parent) : 5 | QObject(parent), 6 | prefs(SharedPreferences::getPreferences(this)) 7 | { 8 | connect(prefs, &SharedPreferences::loaded, 9 | this, &PrefModel::modelChanged); 10 | connect(prefs, &SharedPreferences::changed, 11 | this, &PrefModel::modelChanged); 12 | } 13 | 14 | QStringList PrefModel::model() const 15 | { 16 | auto data = prefs->data(); 17 | QStringList model; 18 | for(auto it = data.constBegin(); it != data.constEnd(); it++) 19 | model.append(it.key() + ": " + it.value().toString()); 20 | return model; 21 | } 22 | 23 | void PrefModel::save(const QString &key, const QVariant &value) 24 | { 25 | prefs->setValue(key, value); 26 | } 27 | 28 | void PrefModel::remove(int index) 29 | { 30 | prefs->remove(prefs->keys().at(index)); 31 | } 32 | -------------------------------------------------------------------------------- /Demo/prefmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef PREFMODEL_H 2 | #define PREFMODEL_H 3 | 4 | #include 5 | #include 6 | 7 | class PrefModel : public QObject 8 | { 9 | Q_OBJECT 10 | 11 | Q_PROPERTY(QStringList model READ model NOTIFY modelChanged) 12 | 13 | public: 14 | explicit PrefModel(QObject *parent = nullptr); 15 | 16 | QStringList model() const; 17 | 18 | public slots: 19 | void save(const QString &key, const QVariant &value); 20 | void remove(int index); 21 | 22 | signals: 23 | void modelChanged(); 24 | 25 | private: 26 | SharedPreferences *prefs; 27 | }; 28 | 29 | #endif // PREFMODEL_H 30 | -------------------------------------------------------------------------------- /Demo/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | qtquickcontrols2.conf 5 | UtilsTab.qml 6 | ContentTab.qml 7 | PreferenceTab.qml 8 | 9 | 10 | -------------------------------------------------------------------------------- /Demo/qtquickcontrols2.conf: -------------------------------------------------------------------------------- 1 | ; This file can be edited to change the style of the application 2 | ; See Styling Qt Quick Controls 2 in the documentation for details: 3 | ; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html 4 | 5 | [Controls] 6 | Style=Material 7 | 8 | [Universal] 9 | Theme=Light 10 | ;Accent=Steel 11 | 12 | [Material] 13 | Theme=Light 14 | ;Accent=BlueGrey 15 | ;Primary=BlueGray 16 | -------------------------------------------------------------------------------- /FileDialog.qml: -------------------------------------------------------------------------------- 1 | import Qt.labs.platform 1.0 as Labs 2 | import de.skycoder42.androidutils 1.1 3 | 4 | Labs.FileDialog { 5 | id: fileDialog 6 | 7 | property alias labsDialog: fileDialog 8 | property var fileChooser: null 9 | 10 | property bool saveFile: false 11 | property alias dialogFlags: fileDialog.options 12 | property int chooserFlags: FileChooser.OpenableFlag 13 | property string mimeType: "*/*" 14 | 15 | fileMode: saveFile ? Labs.FileDialog.SaveFile : Labs.FileDialog.OpenFile 16 | modality: Qt.WindowModal 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Felix Barz 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidUtils 2 | Utils for easy c++ and qml integration of common android features 3 | 4 | ## Features 5 | - Utilizes [androidnative.pri](https://github.com/benlau/androidnative.pri) 6 | - Adds qmake wrappers and additional code to make integration easier 7 | - Automatically registers androidnative when registering AndroidUtils 8 | - Adds a singleton for c++/qml 9 | - Show a toast 10 | - Trigger haptic feedback 11 | - Change the statusbar color 12 | - Adds the `ContentDevice` class (android only) 13 | - QIODevice class for android contents 14 | - Allows you to open genric `content` uris 15 | - This includes local files, as well as data from services like Dropbox, Google Drive, etc. 16 | - Adds the `FileChooser` class 17 | - The Android equivalent of a file dialog 18 | - Shows an android file chooser activity to get/create a content uri 19 | - Provides `FileDialog` qml type that provides a file dialog for all platforms 20 | - On Android, uses the FileChooser 21 | - On other Platforms, the [Qt labs FileDialog](https://doc.qt.io/qt-5/qml-qt-labs-platform-filedialog.html) is used 22 | - Adds the `SharedPreferences` class 23 | - Allows easy C++ access to Android shared preferences 24 | - Notifies of changes in the preferences 25 | 26 | ## Installation 27 | The package is providet as qpm package, [`de.skycoder42.androidutils`](https://www.qpm.io/packages/de.skycoder42.androidutils/index.html). To install: 28 | 29 | 1. Install qpm (See [GitHub - Installing](https://github.com/Cutehacks/qpm/blob/master/README.md#installing), for **windows** see below) 30 | 2. In your projects root directory, run `qpm install de.skycoder42.androidutils` 31 | 3. Include qpm to your project by adding `include(vendor/vendor.pri)` to your `.pro` file 32 | 4. Add the line `apply from: "androidutils.gradle"` at the very bottom of your `build.gradle` file 33 | - That file is located in your android source directory, the directory with your manifest (See [`Demo/android`](Demo/android)) 34 | - If you don't have the folder, you can let QtCreator create one for you. Go to `Projects > [Your Kit] Build > Create Android APK` and create the templates by pressing `Create Templates` 35 | 36 | Check their [GitHub - Usage for App Developers](https://github.com/Cutehacks/qpm/blob/master/README.md#usage-for-app-developers) to learn more about qpm. 37 | 38 | **Important for Windows users:** QPM Version *0.10.0* (the one you can download on the website) is currently broken on windows! It's already fixed in master, but not released yet. Until a newer versions gets released, you can download the latest dev build from here: 39 | - https://storage.googleapis.com/www.qpm.io/download/latest/windows_amd64/qpm.exe 40 | - https://storage.googleapis.com/www.qpm.io/download/latest/windows_386/qpm.exe 41 | 42 | ## Usage 43 | Just include/import (`import de.skycoder42.androidutils 1.0`) the class and use the methods. All the setup and registrations are done automatically. 44 | 45 | ### JNI_OnLoad 46 | By default, AndroidUtils defines the `JNI_OnLoad` for you. However, if you need to define this method yourself, this will create a conflict! To prevent AndroidUtils from creating the method, add the following line **before** including the vendor.pri 47 | 48 | ```pro 49 | CONFIG += noJniOnLoad 50 | include(vendor/vendor.pri) 51 | # ... 52 | ``` 53 | 54 | In that case, make shure you initialize androidnative.pri by adding `AndroidNative::SystemDispatcher::registerNatives();` to your `JNI_onLoad` implementation! 55 | 56 | ### QPM_ROOT 57 | This variable should contain the root directory of qpm, i.e. the `vendor` directory. By default (if you don't set the variable), it is set to `$$_PRO_FILE_PWD_/vendor`. If your qpm vendor folder is not located in the same directory as your pro file, set this variable to the **absolute** path of your vendor folder. If you use the `_PRO_FILE_PWD_`, you can simply make the path relative to this directory, e.g. `QPM_ROOT = $$_PRO_FILE_PWD_/../vendor`. 58 | 59 | ## Android URI permissions 60 | To open generic content uris, you need to get permissions for those. For local files, this are the normal read/write external storage permissions. For other content, i.e. from Dropbox, those permissions are granted for every uri. Those permissions will stay as long as your application is running, but if you want to access them later, e.g. after a restart, you need to persist the permissions. The FileChooser can do this for you, by setting the required `chooserFlags`. If you get Security exceptions when trying to read/write a file, this is what you need to adjust. 61 | -------------------------------------------------------------------------------- /android/androidutils.gradle: -------------------------------------------------------------------------------- 1 | // Obtain androidPackageSourceDir 2 | // androidPackageSourceDir is the absolute path of the folder containing build.gradle and AndroidManifests.xml 3 | // This code also works with androiddeployqt. 4 | 5 | dependencies { 6 | compile "com.android.support:support-core-utils:+" 7 | } 8 | 9 | String addAndroidUtilsPath(String path) { 10 | LinkedHashSet hash = android.sourceSets.main.java.srcDirs; 11 | hash.add(path); 12 | android.sourceSets.main.java.srcDirs = hash; 13 | } 14 | 15 | ext { 16 | addAndroidUtilsPath = this.&addAndroidUtilsPath; 17 | } 18 | -------------------------------------------------------------------------------- /android/src/de/skycoder42/androidutils/AlarmReceiver.java: -------------------------------------------------------------------------------- 1 | package de.skycoder42.androidutils; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.app.PendingIntent; 6 | import android.app.AlarmManager; 7 | import android.util.Log; 8 | import android.support.v4.content.WakefulBroadcastReceiver; 9 | 10 | public class AlarmReceiver extends WakefulBroadcastReceiver { 11 | private static final String TAG = "de.skycoder42.androidutils.AlarmReceiver"; 12 | private static final int ALARM_INTENT_ID = 20; 13 | private static final String CLASS_NAME_EXTRA = "de.skycoder42.androidutils.classname"; 14 | 15 | @Override 16 | public void onReceive(Context context, Intent intent) { 17 | try { 18 | Class classType = Class.forName(intent.getStringExtra(CLASS_NAME_EXTRA)); 19 | Intent serviceIntent = new Intent(context, classType); 20 | startWakefulService(context, serviceIntent); 21 | } catch(ClassNotFoundException e) { 22 | Log.e(TAG, e.getMessage()); 23 | e.printStackTrace(); 24 | } 25 | } 26 | 27 | static public void scheduleAutoCheck(Context context, boolean autoCheck, String serviceClass) { 28 | Intent intent = new Intent(context, AlarmReceiver.class); 29 | intent.putExtra(CLASS_NAME_EXTRA, serviceClass); 30 | 31 | PendingIntent pending = PendingIntent.getBroadcast(context, 32 | ALARM_INTENT_ID, 33 | intent, 34 | PendingIntent.FLAG_UPDATE_CURRENT); 35 | 36 | AlarmManager alarm = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 37 | if(autoCheck) { 38 | alarm.setRepeating(AlarmManager.RTC_WAKEUP, 39 | System.currentTimeMillis() + AlarmManager.INTERVAL_DAY, 40 | AlarmManager.INTERVAL_DAY, 41 | pending); 42 | } else { 43 | alarm.cancel(pending); 44 | pending.cancel(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /android/src/de/skycoder42/androidutils/AndroidUtils.java: -------------------------------------------------------------------------------- 1 | package de.skycoder42.androidutils; 2 | 3 | import java.util.Map; 4 | import android.os.Build; 5 | import android.os.Handler; 6 | import android.content.Context; 7 | import android.app.Activity; 8 | import android.graphics.Color; 9 | import android.view.View; 10 | import android.view.Window; 11 | import android.view.WindowManager; 12 | import org.qtproject.qt5.android.QtNative; 13 | import androidnative.SystemDispatcher; 14 | 15 | public class AndroidUtils { 16 | 17 | static { 18 | SystemDispatcher.addListener(new SystemDispatcher.Listener() { 19 | 20 | public void onDispatched(String name , Map data) { 21 | 22 | if (name.equals("AndroidUtils.hapticFeedback")) 23 | AndroidUtils.hapticFeedback((Integer)data.get("feedbackConstant")); 24 | else if (name.equals("AndroidUtils.setStatusBarColor")) 25 | AndroidUtils.setStatusBarColor((String)data.get("color")); 26 | } 27 | }); 28 | } 29 | 30 | static void hapticFeedback(final int feedbackConstant) { 31 | final Activity activity = QtNative.activity(); 32 | 33 | Runnable runnable = new Runnable () { 34 | public void run() { 35 | View rootView = activity.getWindow().getDecorView().getRootView(); 36 | rootView.performHapticFeedback(feedbackConstant); 37 | }; 38 | }; 39 | activity.runOnUiThread(runnable); 40 | } 41 | 42 | static void setStatusBarColor(String colorName) { 43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 44 | final int color = Color.parseColor(colorName); 45 | final Activity activity = QtNative.activity(); 46 | 47 | Runnable runnable = new Runnable () { 48 | public void run() { 49 | Window window = activity.getWindow(); 50 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 51 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 52 | window.setStatusBarColor(color); 53 | }; 54 | }; 55 | activity.runOnUiThread(runnable); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /android/src/de/skycoder42/androidutils/FileChooser.java: -------------------------------------------------------------------------------- 1 | package de.skycoder42.androidutils; 2 | 3 | import java.util.Map; 4 | import java.util.HashMap; 5 | import android.net.Uri; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.ContentResolver; 9 | import android.app.Activity; 10 | import org.qtproject.qt5.android.QtNative; 11 | import androidnative.SystemDispatcher; 12 | 13 | public class FileChooser { 14 | private static final int CHOOSE_CONTENT_ACTION = 0x1091c657; 15 | private static final int CHOOSE_PERSISTENT_ACTION = 0x1091c658; 16 | 17 | private static final String GET_CONTENT_MESSAGE = "AndroidUtils.FileChooser.getContent"; 18 | private static final String OPEN_DOCUMENT_MESSAGE = "AndroidUtils.FileChooser.openDocument"; 19 | private static final String CREATE_DOCUMENT_MESSAGE = "AndroidUtils.FileChooser.createDocument"; 20 | private static final String CONTENT_CHOOSEN_MESSAGE = "AndroidUtils.FileChooser.contentChoosen"; 21 | 22 | static { 23 | SystemDispatcher.addListener(new SystemDispatcher.Listener() { 24 | public void onDispatched(String type , Map message) { 25 | if (type.equals(GET_CONTENT_MESSAGE)) { 26 | getContent((String)message.get("title"), 27 | (String)message.get("mime"), 28 | (Boolean)message.get("openable"), 29 | (Boolean)message.get("localOnly"), 30 | (Boolean)message.get("grantWrite")); 31 | } else if (type.equals(OPEN_DOCUMENT_MESSAGE)) { 32 | openDocument((String)message.get("title"), 33 | (String)message.get("mime"), 34 | (String)message.get("url"), 35 | (Boolean)message.get("openable"), 36 | (Boolean)message.get("grantWrite"), 37 | (Boolean)message.get("persistPermissions")); 38 | } else if (type.equals(CREATE_DOCUMENT_MESSAGE)) { 39 | createDocument((String)message.get("title"), 40 | (String)message.get("mime"), 41 | (String)message.get("url"), 42 | (String)message.get("name"), 43 | (Boolean)message.get("openable"), 44 | (Boolean)message.get("persistPermissions")); 45 | } else if (type.equals(SystemDispatcher.ACTIVITY_RESULT_MESSAGE)) { 46 | onActivityResult((Integer)message.get("requestCode"), 47 | (Integer)message.get("resultCode"), 48 | (Intent)message.get("data")); 49 | } 50 | } 51 | }); 52 | } 53 | 54 | static private void getContent(final String title, String mime, boolean openable, boolean localOnly, boolean grantWrite) { 55 | final Activity activity = QtNative.activity(); 56 | 57 | final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 58 | intent.setType(mime); 59 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 60 | if(grantWrite) 61 | intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 62 | if(openable) 63 | intent.addCategory(Intent.CATEGORY_OPENABLE); 64 | if(localOnly) 65 | intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); 66 | 67 | Runnable runnable = new Runnable () { 68 | public void run() { 69 | activity.startActivityForResult( 70 | Intent.createChooser(intent, title), 71 | CHOOSE_CONTENT_ACTION); 72 | }; 73 | }; 74 | activity.runOnUiThread(runnable); 75 | } 76 | 77 | static private void openDocument(final String title, String mime, String url, boolean openable, boolean grantWrite, final boolean persistPermissions) { 78 | final Activity activity = QtNative.activity(); 79 | 80 | final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); 81 | intent.setType(mime); 82 | intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 83 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 84 | if(grantWrite) 85 | intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 86 | if(openable) 87 | intent.addCategory(Intent.CATEGORY_OPENABLE); 88 | //TODO add in Android O 89 | // if(!url.isEmpty()) 90 | // intent.putExtra(Intent.EXTRA_INITIAL_URI, Uri.parse(url)); 91 | 92 | Runnable runnable = new Runnable () { 93 | public void run() { 94 | activity.startActivityForResult( 95 | Intent.createChooser(intent, title), 96 | persistPermissions ? CHOOSE_PERSISTENT_ACTION : CHOOSE_CONTENT_ACTION); 97 | }; 98 | }; 99 | activity.runOnUiThread(runnable); 100 | } 101 | 102 | static private void createDocument(final String title, String mime, String url, String name, boolean openable, final boolean persistPermissions) { 103 | final Activity activity = QtNative.activity(); 104 | 105 | final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); 106 | intent.setType(mime); 107 | intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 108 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 109 | intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 110 | if(openable) 111 | intent.addCategory(Intent.CATEGORY_OPENABLE); 112 | //TODO add in Android O 113 | // if(!url.isEmpty()) 114 | // intent.putExtra(Intent.EXTRA_INITIAL_URI, Uri.parse(url)); 115 | if(!name.isEmpty()) 116 | intent.putExtra(Intent.EXTRA_TITLE, name); 117 | 118 | Runnable runnable = new Runnable () { 119 | public void run() { 120 | activity.startActivityForResult( 121 | Intent.createChooser(intent, title), 122 | persistPermissions ? CHOOSE_PERSISTENT_ACTION : CHOOSE_CONTENT_ACTION); 123 | }; 124 | }; 125 | activity.runOnUiThread(runnable); 126 | } 127 | 128 | static private void onActivityResult(int requestCode, int resultCode, Intent data) { 129 | if(requestCode == CHOOSE_CONTENT_ACTION || 130 | requestCode == CHOOSE_PERSISTENT_ACTION) { 131 | Map reply = new HashMap(); 132 | if (resultCode == Activity.RESULT_OK) { 133 | Uri uri = data.getData(); 134 | 135 | if(requestCode == CHOOSE_PERSISTENT_ACTION) { 136 | int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 137 | QtNative.activity() 138 | .getContentResolver() 139 | .takePersistableUriPermission(uri, takeFlags); 140 | } 141 | 142 | reply.put("uri", uri.toString()); 143 | reply.put("success", true); 144 | } else 145 | reply.put("success", false); 146 | SystemDispatcher.dispatch(CONTENT_CHOOSEN_MESSAGE, reply); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /android/src/de/skycoder42/androidutils/PrefHelper.java: -------------------------------------------------------------------------------- 1 | package de.skycoder42.androidutils; 2 | 3 | import java.util.Map; 4 | import java.util.AbstractMap; 5 | import java.util.HashMap; 6 | import android.content.Context; 7 | import android.content.SharedPreferences; 8 | import org.qtproject.qt5.android.QtNative; 9 | import androidnative.SystemDispatcher; 10 | 11 | public class PrefHelper { 12 | private static final String DATA_CHANGED_MESSAGE = "AndroidUtils.PrefHelper.changed."; 13 | private static final String DATA_LOADED_MESSAGE = "AndroidUtils.PrefHelper.loaded."; 14 | 15 | static { 16 | SystemDispatcher.addListener(new SystemDispatcher.Listener() { 17 | 18 | public void onDispatched(String name , Map data) { 19 | if (name.equals("AndroidUtils.PrefHelper.getPrefs")) 20 | getPrefs((String)data.get("id")); 21 | else if (name.equals("AndroidUtils.PrefHelper.getSharedPrefs")) 22 | getSharedPrefs((String)data.get("id"), (String)data.get("name")); 23 | else if (name.equals("AndroidUtils.PrefHelper.remPrefs")) 24 | remPrefs((String)data.get("id")); 25 | else if (name.equals("AndroidUtils.PrefHelper.save")) 26 | save((String)data.get("id"), (String)data.get("key"), data.get("value")); 27 | else if (name.equals("AndroidUtils.PrefHelper.remove")) 28 | remove((String)data.get("id"), (String)data.get("key")); 29 | } 30 | }); 31 | } 32 | 33 | interface HelperListener extends SharedPreferences.OnSharedPreferenceChangeListener { 34 | SharedPreferences preferences(); 35 | } 36 | 37 | static Map _activePrefs = new HashMap<>(); 38 | 39 | static void getPrefs(String id) { 40 | registerPrefs(id, QtNative.activity().getPreferences(Context.MODE_PRIVATE)); 41 | } 42 | 43 | static void getSharedPrefs(String id, String name) { 44 | Context c = QtNative.activity(); 45 | if(c == null) 46 | c = QtNative.service(); 47 | registerPrefs(id, c.getSharedPreferences(name, Context.MODE_PRIVATE)); 48 | } 49 | 50 | static void remPrefs(final String id) { 51 | HelperListener listener = _activePrefs.remove(id); 52 | listener.preferences().unregisterOnSharedPreferenceChangeListener(listener); 53 | } 54 | 55 | static void save(String id, String key, Object value) { 56 | SharedPreferences.Editor prefs = _activePrefs.get(id).preferences().edit(); 57 | if (value.getClass() == Boolean.class) { 58 | prefs.putBoolean(key, (Boolean)value); 59 | } else if (value.getClass() == Float.class) { 60 | prefs.putFloat(key, (Float)value); 61 | } else if (value.getClass() == Integer.class) { 62 | prefs.putInt(key, (Integer)value); 63 | } else if (value.getClass() == Long.class) { 64 | prefs.putLong(key, (Long)value); 65 | } else if (value.getClass() == String.class) { 66 | prefs.putString(key, (String)value); 67 | } 68 | prefs.apply(); 69 | } 70 | 71 | static void remove(String id, String key) { 72 | SharedPreferences.Editor prefs = _activePrefs.get(id).preferences().edit(); 73 | prefs.remove(key); 74 | prefs.apply(); 75 | } 76 | 77 | static void registerPrefs(final String id, final SharedPreferences preferences) { 78 | HelperListener listener = new HelperListener() { 79 | 80 | public SharedPreferences preferences() { 81 | return preferences; 82 | } 83 | 84 | public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 85 | if(prefs.contains(key)) { 86 | Map data = prefs.getAll(); 87 | Map reply = new HashMap(); 88 | reply.put("key", key); 89 | reply.put("value", data.get(key)); 90 | SystemDispatcher.dispatch(DATA_CHANGED_MESSAGE + id, reply); 91 | } else { 92 | Map reply = new HashMap(); 93 | reply.put("key", key); 94 | reply.put("removed", true); 95 | SystemDispatcher.dispatch(DATA_CHANGED_MESSAGE + id, reply); 96 | } 97 | } 98 | }; 99 | _activePrefs.put(id, listener); 100 | preferences.registerOnSharedPreferenceChangeListener(listener); 101 | 102 | SystemDispatcher.dispatch(DATA_LOADED_MESSAGE + id, preferences.getAll()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /androidutils.cpp: -------------------------------------------------------------------------------- 1 | #include "androidutils.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #ifdef Q_OS_ANDROID 7 | #include 8 | #else 9 | #include 10 | #endif 11 | #include "filechooser.h" 12 | 13 | static void setupUtils(); 14 | static QObject *createQmlSingleton(QQmlEngine *qmlEngine, QJSEngine *jsEngine); 15 | 16 | Q_COREAPP_STARTUP_FUNCTION(setupUtils) 17 | 18 | AndroidUtils *AndroidUtils::_instance = nullptr; 19 | 20 | void AndroidUtils::javaThrow() 21 | { 22 | #ifdef Q_OS_ANDROID 23 | QAndroidJniEnvironment env; 24 | if (env->ExceptionCheck()) { 25 | auto exception = QAndroidJniObject::fromLocalRef(env->ExceptionOccurred()); 26 | JavaException exc; 27 | exc._what = exception.callObjectMethod("getLocalizedMessage", "()Ljava/lang/String;").toString().toUtf8(); 28 | 29 | QAndroidJniObject stringWriter("java/io/StringWriter"); 30 | QAndroidJniObject printWriter("java/io/PrintWriter", "(Ljava/lang/Writer;)V", stringWriter.object()); 31 | exception.callMethod("printStackTrace", "(Ljava/lang/PrintWriter;)V", printWriter.object()); 32 | exc._stackTrace = stringWriter.callObjectMethod("toString", "()Ljava/lang/String;").toString().toUtf8(); 33 | 34 | env->ExceptionClear(); 35 | 36 | throw exc; 37 | } 38 | #endif 39 | } 40 | 41 | AndroidUtils::AndroidUtils(QObject *parent) : 42 | QObject(parent) 43 | {} 44 | 45 | AndroidUtils *AndroidUtils::instance() 46 | { 47 | if(!_instance) 48 | _instance = new AndroidUtils(qApp); 49 | return _instance; 50 | } 51 | 52 | void AndroidUtils::setStatusBarColor(const QColor &color) 53 | { 54 | #ifdef Q_OS_ANDROID 55 | AndroidNative::SystemDispatcher::instance()->dispatch("AndroidUtils.setStatusBarColor", { 56 | {"color", color.name()} 57 | }); 58 | #else 59 | Q_UNUSED(color); 60 | #endif 61 | } 62 | 63 | void AndroidUtils::showToast(const QString &message, bool showLong) 64 | { 65 | #ifdef Q_OS_ANDROID 66 | AndroidNative::SystemDispatcher::instance()->dispatch("androidnative.Toast.showToast", { 67 | {"text", message}, 68 | {"longLength", showLong} 69 | }); 70 | #else 71 | Q_UNUSED(showLong) 72 | qInfo() << message; 73 | #endif 74 | } 75 | 76 | void AndroidUtils::hapticFeedback(HapticFeedbackConstant constant) 77 | { 78 | #ifdef Q_OS_ANDROID 79 | AndroidNative::SystemDispatcher::instance()->dispatch("AndroidUtils.hapticFeedback", { 80 | {"feedbackConstant", (int)constant} 81 | }); 82 | #else 83 | Q_UNUSED(constant); 84 | #endif 85 | } 86 | 87 | static void setupUtils() 88 | { 89 | AndroidNative::SystemDispatcher::instance()->loadClass("androidnative.Toast"); 90 | AndroidNative::SystemDispatcher::instance()->loadClass("de.skycoder42.androidutils.AndroidUtils"); 91 | 92 | qmlRegisterSingletonType("de.skycoder42.androidutils", 1, 1, "AndroidUtils", createQmlSingleton); 93 | qmlRegisterType("de.skycoder42.androidutils", 1, 1, "FileChooser"); 94 | //qmlProtectModule("de.skycoder42.quickextras", 1); 95 | } 96 | 97 | static QObject *createQmlSingleton(QQmlEngine *qmlEngine, QJSEngine *jsEngine) 98 | { 99 | Q_UNUSED(qmlEngine) 100 | Q_UNUSED(jsEngine) 101 | return new AndroidUtils(qmlEngine); 102 | } 103 | 104 | 105 | 106 | JavaException::JavaException() : 107 | _what(), 108 | _stackTrace() 109 | {} 110 | 111 | const char *JavaException::what() const noexcept 112 | { 113 | return _what.constData(); 114 | } 115 | 116 | const QByteArray JavaException::printStackTrace() const noexcept 117 | { 118 | return _stackTrace; 119 | } 120 | 121 | void JavaException::raise() const 122 | { 123 | throw *this; 124 | } 125 | 126 | QException *JavaException::clone() const 127 | { 128 | auto e = new JavaException(); 129 | e->_what = _what; 130 | e->_stackTrace = _stackTrace; 131 | return e; 132 | } 133 | -------------------------------------------------------------------------------- /androidutils.h: -------------------------------------------------------------------------------- 1 | #ifndef ANDROIDUTILS_H 2 | #define ANDROIDUTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class JavaException : public QException 10 | { 11 | friend class AndroidUtils; 12 | public: 13 | JavaException(); 14 | 15 | const char *what() const noexcept override; 16 | const QByteArray printStackTrace() const noexcept; 17 | 18 | void raise() const override; 19 | QException *clone() const override; 20 | 21 | private: 22 | QByteArray _what; 23 | QByteArray _stackTrace; 24 | }; 25 | 26 | class AndroidUtils : public QObject 27 | { 28 | Q_OBJECT 29 | 30 | public: 31 | enum HapticFeedbackConstant { 32 | LongPress = 0, 33 | VirtualKey = 1, 34 | KeyboardTap = 3, 35 | ClockTick = 4, 36 | ContextClick = 6 37 | }; 38 | Q_ENUM(HapticFeedbackConstant) 39 | 40 | static void javaThrow(); 41 | 42 | AndroidUtils(QObject *parent = nullptr); 43 | static AndroidUtils *instance(); 44 | 45 | public slots: 46 | void setStatusBarColor(const QColor &color); 47 | void showToast(const QString &message, bool showLong = false); 48 | void hapticFeedback(HapticFeedbackConstant constant= LongPress); 49 | 50 | private: 51 | static AndroidUtils *_instance; 52 | }; 53 | 54 | #endif // ANDROIDUTILS_H 55 | -------------------------------------------------------------------------------- /androidutils_de.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ContentDevice 6 | 7 | 8 | You can only open ContentDevice with QIODevice::ReadOnly OR QIODevice::WriteOnly. Other flags are not supported 9 | Ein ContentDevice can nur mit QIODevice::ReadOnly ODER QIODevice::WriteOnly geöffnet werden. Andere modi werden nicht unterstützt 10 | 11 | 12 | 13 | FileChooser 14 | 15 | 16 | 17 | Open File 18 | Datei öffnen 19 | 20 | 21 | 22 | Save File 23 | Datei speichern 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /androidutils_jni.cpp: -------------------------------------------------------------------------------- 1 | #if defined(Q_OS_ANDROID) 2 | #include 3 | 4 | JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { 5 | Q_UNUSED(vm); 6 | // It must call this function within JNI_OnLoad to enable System Dispatcher 7 | AndroidNative::SystemDispatcher::registerNatives(); 8 | return JNI_VERSION_1_6; 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /androidutils_template.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ContentDevice 6 | 7 | 8 | You can only open ContentDevice with QIODevice::ReadOnly OR QIODevice::WriteOnly. Other flags are not supported 9 | 10 | 11 | 12 | 13 | FileChooser 14 | 15 | 16 | 17 | Open File 18 | 19 | 20 | 21 | 22 | Save File 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /contentdevice.cpp: -------------------------------------------------------------------------------- 1 | #include "androidutils.h" 2 | #include "contentdevice.h" 3 | #include 4 | #include 5 | 6 | ContentDevice::ContentDevice(QObject *parent) : 7 | QIODevice(parent), 8 | _context(QtAndroid::androidContext()), 9 | _url() 10 | { 11 | Q_ASSERT(_context.isValid()); 12 | } 13 | 14 | ContentDevice::ContentDevice(const QUrl &url, QObject *parent) : 15 | QIODevice(parent), 16 | _context(QtAndroid::androidContext()), 17 | _url(url) 18 | { 19 | Q_ASSERT(_context.isValid()); 20 | } 21 | 22 | ContentDevice::ContentDevice(const QAndroidJniObject &context, const QUrl &url, QObject *parent) : 23 | QIODevice(parent), 24 | _context(context), 25 | _url(url) 26 | { 27 | Q_ASSERT(_context.isValid()); 28 | } 29 | 30 | void ContentDevice::setContext(const QAndroidJniObject &context) 31 | { 32 | _context = context; 33 | Q_ASSERT(_context.isValid()); 34 | } 35 | 36 | bool ContentDevice::isSequential() const 37 | { 38 | return true; 39 | } 40 | 41 | bool ContentDevice::open(QIODevice::OpenMode mode) 42 | { 43 | if(!QIODevice::open(mode)) 44 | return false; 45 | 46 | QAndroidJniEnvironment env; 47 | try { 48 | auto uri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", 49 | "parse", 50 | "(Ljava/lang/String;)Landroid/net/Uri;", 51 | QAndroidJniObject::fromString(_url.toString(QUrl::FullyEncoded)).object()); 52 | AndroidUtils::javaThrow(); 53 | if(!uri.isValid()) 54 | return false; 55 | 56 | auto contentResolver = _context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); 57 | AndroidUtils::javaThrow(); 58 | if(!contentResolver.isValid()) 59 | return false; 60 | 61 | switch(mode) { 62 | case QIODevice::ReadOnly: 63 | _stream = contentResolver.callObjectMethod("openInputStream", 64 | "(Landroid/net/Uri;)Ljava/io/InputStream;", 65 | uri.object()); 66 | break; 67 | case QIODevice::WriteOnly: 68 | _stream = contentResolver.callObjectMethod("openOutputStream", 69 | "(Landroid/net/Uri;)Ljava/io/OutputStream;", 70 | uri.object()); 71 | break; 72 | default: 73 | setErrorString(tr("You can only open ContentDevice with QIODevice::ReadOnly OR QIODevice::WriteOnly. Other flags are not supported")); 74 | QIODevice::close(); 75 | return false; 76 | } 77 | 78 | AndroidUtils::javaThrow(); 79 | if(!_stream.isValid()) 80 | return false; 81 | 82 | return true; 83 | } catch(QException &e) { 84 | QIODevice::close(); 85 | setErrorString(e.what()); 86 | return false; 87 | } 88 | } 89 | 90 | void ContentDevice::close() 91 | { 92 | if(_stream.isValid()) { 93 | _stream.callMethod("close"); 94 | _stream = QAndroidJniObject(); 95 | } 96 | QIODevice::close(); 97 | } 98 | 99 | qint64 ContentDevice::bytesAvailable() const 100 | { 101 | if(openMode().testFlag(QIODevice::ReadOnly) && _stream.isValid()) 102 | return (qint64)_stream.callMethod("available"); 103 | 104 | return -1; 105 | } 106 | 107 | void ContentDevice::flush() 108 | { 109 | if(openMode().testFlag(QIODevice::WriteOnly) && _stream.isValid()) 110 | _stream.callMethod("flush"); 111 | } 112 | 113 | QUrl ContentDevice::url() const 114 | { 115 | return _url; 116 | } 117 | 118 | void ContentDevice::setUrl(QUrl url) 119 | { 120 | if (_url == url) 121 | return; 122 | 123 | _url = url; 124 | emit urlChanged(url); 125 | } 126 | 127 | qint64 ContentDevice::readData(char *data, qint64 maxlen) 128 | { 129 | QAndroidJniEnvironment env; 130 | auto array = env->NewByteArray((jsize)maxlen); 131 | try { 132 | auto cnt = _stream.callMethod("read", "([B)I", array); 133 | AndroidUtils::javaThrow(); 134 | if(cnt > 0) 135 | env->GetByteArrayRegion(array, 0, cnt, reinterpret_cast(data)); 136 | env->DeleteLocalRef(array); 137 | return (qint64)cnt; 138 | } catch(QException &e) { 139 | setErrorString(e.what()); 140 | env->DeleteLocalRef(array); 141 | return -1; 142 | } 143 | } 144 | 145 | qint64 ContentDevice::writeData(const char *data, qint64 len) 146 | { 147 | QAndroidJniEnvironment env; 148 | auto array = env->NewByteArray((jsize)len); 149 | env->SetByteArrayRegion (array, 0, len, reinterpret_cast(data)); 150 | try { 151 | _stream.callMethod("write", "([B)V", array); 152 | AndroidUtils::javaThrow(); 153 | env->DeleteLocalRef(array); 154 | return len; 155 | } catch(QException &e) { 156 | setErrorString(e.what()); 157 | env->DeleteLocalRef(array); 158 | return -1; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /contentdevice.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTENTDEVICE_H 2 | #define CONTENTDEVICE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class ContentDevice : public QIODevice 9 | { 10 | Q_OBJECT 11 | 12 | Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) 13 | 14 | 15 | public: 16 | explicit ContentDevice(QObject *parent = nullptr); 17 | explicit ContentDevice(const QUrl &url, QObject *parent = nullptr); 18 | explicit ContentDevice(const QAndroidJniObject &context, const QUrl &url, QObject *parent = nullptr); 19 | 20 | void setContext(const QAndroidJniObject &context); 21 | 22 | bool isSequential() const override; 23 | bool open(OpenMode mode) override; 24 | void close() override; 25 | qint64 bytesAvailable() const override; 26 | void flush(); 27 | 28 | QUrl url() const; 29 | 30 | public slots: 31 | void setUrl(QUrl url); 32 | 33 | signals: 34 | void urlChanged(QUrl url); 35 | 36 | protected: 37 | qint64 readData(char *data, qint64 maxlen) override; 38 | qint64 writeData(const char *data, qint64 len) override; 39 | 40 | private: 41 | QAndroidJniObject _context; 42 | QUrl _url; 43 | 44 | QAndroidJniObject _stream; 45 | }; 46 | 47 | #endif // CONTENTDEVICE_H 48 | -------------------------------------------------------------------------------- /de_skycoder42_androidutils.prc: -------------------------------------------------------------------------------- 1 | DISTFILES += \ 2 | $$PWD/android/androidutils.gradle \ 3 | $$PWD/android/src/de/skycoder42/androidutils/AlarmReceiver.java \ 4 | $$PWD/android/src/de/skycoder42/androidutils/AndroidUtils.java \ 5 | $$PWD/android/src/de/skycoder42/androidutils/FileChooser.java \ 6 | $$PWD/android/src/de/skycoder42/androidutils/PrefHelper.java 7 | 8 | android { 9 | QT *= androidextras 10 | 11 | !noJniOnLoad: SOURCES += androidutils_jni.cpp 12 | 13 | #isEmpty(ANDROID_NATIVE_PATH): ANDROID_NATIVE_PATH = $$_PRO_FILE_PWD_/vendor/android/native/pri 14 | isEmpty(ANDROID_NATIVE_PATH): ANDROID_NATIVE_PATH = $$PWD/../../android.native.pri/0.0.5/ #assume qpmx install, both as qpm package 15 | 16 | copygradle.path = / 17 | copygradle.files = $$PWD/android/androidutils.gradle 18 | 19 | setupnative.path = / 20 | setupnative.files = 21 | setupnative.commands = echo 'addAndroidUtilsPath\\(\\\"$$ANDROID_NATIVE_PATH/java/src/\\\"\\)' >> $(INSTALL_ROOT)/androidutils.gradle 22 | setupnative.depends = install_copygradle 23 | 24 | setupgradle.path = / 25 | setupgradle.files = 26 | setupgradle.commands = echo 'addAndroidUtilsPath\\(\\\"$$PWD/android/src\\\"\\)' >> $(INSTALL_ROOT)/androidutils.gradle 27 | setupgradle.depends = install_copygradle 28 | 29 | INSTALLS += copygradle setupnative setupgradle 30 | } 31 | -------------------------------------------------------------------------------- /de_skycoder42_androidutils.pri: -------------------------------------------------------------------------------- 1 | QT += qml quick 2 | 3 | HEADERS += \ 4 | $$PWD/androidutils.h \ 5 | $$PWD/filechooser.h \ 6 | $$PWD/sharedpreferences.h 7 | 8 | SOURCES += \ 9 | $$PWD/androidutils.cpp \ 10 | $$PWD/filechooser.cpp \ 11 | $$PWD/sharedpreferences.cpp 12 | 13 | RESOURCES += \ 14 | $$PWD/de_skycoder42_androidutils.qrc 15 | 16 | INCLUDEPATH += $$PWD 17 | 18 | TRANSLATIONS += $$PWD/androidutils_de.ts \ 19 | $$PWD/androidutils_template.ts 20 | 21 | android { 22 | HEADERS += $$PWD/contentdevice.h 23 | SOURCES += $$PWD/contentdevice.cpp 24 | } 25 | 26 | !qpmx_static: include($$PWD/de_skycoder42_androidutils.prc) 27 | else:android: QT *= androidextras 28 | -------------------------------------------------------------------------------- /de_skycoder42_androidutils.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | qmldir 4 | FileDialog.qml 5 | AndroidFileDialog.qml 6 | 7 | 8 | -------------------------------------------------------------------------------- /filechooser.cpp: -------------------------------------------------------------------------------- 1 | #include "filechooser.h" 2 | #include 3 | 4 | const QString FileChooser::ContentChoosenMessage = QStringLiteral("AndroidUtils.FileChooser.contentChoosen"); 5 | 6 | FileChooser::FileChooser(QObject *parent) : 7 | QObject(parent), 8 | _title(), 9 | _contentUrl(), 10 | _type(OpenDocument), 11 | _mimeType(QStringLiteral("*/*")), 12 | _flags(OpenableFlag) 13 | { 14 | auto dispatcher = AndroidNative::SystemDispatcher::instance(); 15 | dispatcher->loadClass(QStringLiteral("de.skycoder42.androidutils.FileChooser")); 16 | connect(dispatcher, &AndroidNative::SystemDispatcher::dispatched, 17 | this, &FileChooser::onDispatched, 18 | Qt::QueuedConnection); 19 | } 20 | 21 | QString FileChooser::title() const 22 | { 23 | return _title; 24 | } 25 | 26 | QUrl FileChooser::contentUrl() const 27 | { 28 | return _contentUrl; 29 | } 30 | 31 | FileChooser::ChooserType FileChooser::type() const 32 | { 33 | return _type; 34 | } 35 | 36 | QString FileChooser::mimeType() const 37 | { 38 | return _mimeType; 39 | } 40 | 41 | FileChooser::ChooserFlags FileChooser::chooserFlags() const 42 | { 43 | return _flags; 44 | } 45 | 46 | void FileChooser::setContentUrl(QUrl contentUrl) 47 | { 48 | if (_contentUrl == contentUrl) 49 | return; 50 | 51 | _contentUrl = contentUrl; 52 | emit contentUrlChanged(contentUrl); 53 | } 54 | 55 | void FileChooser::setType(FileChooser::ChooserType type) 56 | { 57 | if (_type == type) 58 | return; 59 | 60 | _type = type; 61 | emit typeChanged(type); 62 | } 63 | 64 | void FileChooser::setMimeType(QString mimeType) 65 | { 66 | if (_mimeType == mimeType) 67 | return; 68 | 69 | _mimeType = mimeType; 70 | emit mimeTypeChanged(mimeType); 71 | } 72 | 73 | void FileChooser::setChooserFlags(ChooserFlags chooserFlags) 74 | { 75 | if (_flags == chooserFlags) 76 | return; 77 | 78 | _flags = chooserFlags; 79 | emit chooserFlagsChanged(chooserFlags); 80 | } 81 | 82 | void FileChooser::onDispatched(const QString &message, const QVariantMap &data) 83 | { 84 | if(message == ContentChoosenMessage) { 85 | if(data.value(QStringLiteral("success"), false).toBool()) { 86 | _contentUrl = data.value(QStringLiteral("uri")).toString(); 87 | emit contentUrlChanged(_contentUrl); 88 | emit accepted(); 89 | } else 90 | emit rejected(); 91 | } 92 | } 93 | 94 | void FileChooser::open() 95 | { 96 | QString message; 97 | QVariantMap data; 98 | 99 | switch (_type) { 100 | case FileChooser::GetContent: 101 | message = QStringLiteral("AndroidUtils.FileChooser.getContent"); 102 | data = { 103 | {"title", _title.isEmpty() ? tr("Open File") : _title}, 104 | {"mime", _mimeType}, 105 | {"openable", _flags.testFlag(OpenableFlag)}, 106 | {"localOnly", _flags.testFlag(LocalOnlyFlag)}, 107 | {"grantWrite", _flags.testFlag(AlwaysGrantWriteFlag)} 108 | }; 109 | break; 110 | case FileChooser::OpenDocument: 111 | message = QStringLiteral("AndroidUtils.FileChooser.openDocument"); 112 | data = { 113 | {"title", _title.isEmpty() ? tr("Open File") : _title}, 114 | {"mime", _mimeType}, 115 | {"url", _contentUrl.toString()}, 116 | {"openable", _flags.testFlag(OpenableFlag)}, 117 | {"grantWrite", _flags.testFlag(AlwaysGrantWriteFlag)}, 118 | {"persistPermissions", _flags.testFlag(PersistPermissionsFlag)} 119 | }; 120 | break; 121 | case FileChooser::CreateDocument: 122 | message = QStringLiteral("AndroidUtils.FileChooser.createDocument"); 123 | data = { 124 | {"title", _title.isEmpty() ? tr("Save File") : _title}, 125 | {"mime", _mimeType}, 126 | {"url", _contentUrl.toString()}, 127 | {"name", _contentUrl.fileName()}, 128 | {"openable", _flags.testFlag(OpenableFlag)}, 129 | {"persistPermissions", _flags.testFlag(PersistPermissionsFlag)} 130 | }; 131 | break; 132 | default: 133 | Q_UNREACHABLE(); 134 | break; 135 | } 136 | 137 | AndroidNative::SystemDispatcher::instance()->dispatch(message, data); 138 | } 139 | 140 | void FileChooser::setTitle(QString title) 141 | { 142 | if (_title == title) 143 | return; 144 | 145 | _title = title; 146 | emit titleChanged(title); 147 | } 148 | -------------------------------------------------------------------------------- /filechooser.h: -------------------------------------------------------------------------------- 1 | #ifndef FILECHOOSER_H 2 | #define FILECHOOSER_H 3 | 4 | #include 5 | #include 6 | 7 | class FileChooser : public QObject 8 | { 9 | Q_OBJECT 10 | 11 | Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) 12 | 13 | Q_PROPERTY(QUrl contentUrl READ contentUrl WRITE setContentUrl NOTIFY contentUrlChanged) 14 | Q_PROPERTY(ChooserType type READ type WRITE setType NOTIFY typeChanged) 15 | Q_PROPERTY(QString mimeType READ mimeType WRITE setMimeType NOTIFY mimeTypeChanged) 16 | Q_PROPERTY(ChooserFlags chooserFlags READ chooserFlags WRITE setChooserFlags NOTIFY chooserFlagsChanged) 17 | 18 | public: 19 | enum ChooserType { 20 | GetContent = 0, 21 | OpenDocument = 1, 22 | CreateDocument = 2 23 | }; 24 | Q_ENUM(ChooserType) 25 | 26 | enum ChooserFlag { 27 | OpenableFlag = 0x01, 28 | LocalOnlyFlag = 0x02, 29 | AlwaysGrantWriteFlag = 0x04, 30 | PersistPermissionsFlag = 0x08 31 | }; 32 | Q_DECLARE_FLAGS(ChooserFlags, ChooserFlag) 33 | Q_FLAG(ChooserFlags) 34 | 35 | explicit FileChooser(QObject *parent = nullptr); 36 | 37 | QString title() const; 38 | QUrl contentUrl() const; 39 | ChooserType type() const; 40 | QString mimeType() const; 41 | ChooserFlags chooserFlags() const; 42 | 43 | public slots: 44 | void open(); 45 | 46 | void setTitle(QString title); 47 | void setContentUrl(QUrl contentUrl); 48 | void setType(ChooserType type); 49 | void setMimeType(QString mimeType); 50 | void setChooserFlags(ChooserFlags chooserFlags); 51 | 52 | signals: 53 | void accepted(); 54 | void rejected(); 55 | 56 | void titleChanged(QString title); 57 | void contentUrlChanged(QUrl contentUrl); 58 | void typeChanged(ChooserType type); 59 | void mimeTypeChanged(QString mimeType); 60 | void chooserFlagsChanged(ChooserFlags chooserFlags); 61 | 62 | private slots: 63 | void onDispatched(const QString &message, const QVariantMap &data); 64 | 65 | private: 66 | static const QString ContentChoosenMessage; 67 | 68 | QString _title; 69 | QUrl _contentUrl; 70 | ChooserType _type; 71 | QString _mimeType; 72 | ChooserFlags _flags; 73 | }; 74 | 75 | #endif // FILECHOOSER_H 76 | -------------------------------------------------------------------------------- /qmldir: -------------------------------------------------------------------------------- 1 | module de.skycoder42.androidutils 2 | 3 | FileDialog 1.1 FileDialog.qml 4 | -------------------------------------------------------------------------------- /qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "skycoder42.de@gmx.de", 4 | "name": "Skycoder" 5 | }, 6 | "dependencies": [ 7 | "android.native.pri@0.0.5" 8 | ], 9 | "description": "Utils for easy c++ and qml integration of common android features", 10 | "license": "BSD_3_CLAUSE", 11 | "name": "de.skycoder42.androidutils", 12 | "pri_filename": "de_skycoder42_androidutils.pri", 13 | "repository": { 14 | "type": "GITHUB", 15 | "url": "https://github.com/Skycoder42/AndroidUtils.git" 16 | }, 17 | "version": { 18 | "fingerprint": "", 19 | "label": "1.2.2", 20 | "revision": "" 21 | }, 22 | "webpage": "https://github.com/Skycoder42/AndroidUtils" 23 | } 24 | -------------------------------------------------------------------------------- /qpmx.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "package": "android.native.pri", 5 | "provider": "qpm", 6 | "version": "0.0.5" 7 | } 8 | ], 9 | "license": { 10 | "file": "LICENSE", 11 | "name": "BSD_3_CLAUSE" 12 | }, 13 | "prcFile": "de_skycoder42_androidutils.prc", 14 | "priFile": "de_skycoder42_androidutils.pri", 15 | "priIncludes": [ 16 | ], 17 | "publishers": { 18 | "qpm": { 19 | "author": { 20 | "email": "skycoder42.de@gmx.de", 21 | "name": "Skycoder" 22 | }, 23 | "description": "Utils for easy c++ and qml integration of common android features", 24 | "name": "de.skycoder42.androidutils", 25 | "repository": { 26 | "type": "GITHUB", 27 | "url": "https://github.com/Skycoder42/AndroidUtils.git" 28 | }, 29 | "webpage": "https://github.com/Skycoder42/AndroidUtils" 30 | } 31 | }, 32 | "source": false 33 | } 34 | -------------------------------------------------------------------------------- /sharedpreferences.cpp: -------------------------------------------------------------------------------- 1 | #include "sharedpreferences.h" 2 | 3 | #include 4 | #include 5 | 6 | SharedPreferences::SharedPreferences(const QString &id, QObject *parent) : 7 | QObject(parent), 8 | _id(id), 9 | _data() 10 | { 11 | AndroidNative::SystemDispatcher::instance()->loadClass(QStringLiteral("de.skycoder42.androidutils.PrefHelper")); 12 | 13 | connect(AndroidNative::SystemDispatcher::instance(), &AndroidNative::SystemDispatcher::dispatched, 14 | this, &SharedPreferences::dispatched, 15 | Qt::QueuedConnection); 16 | } 17 | 18 | SharedPreferences::~SharedPreferences() 19 | { 20 | AndroidNative::SystemDispatcher::instance()->dispatch(QStringLiteral("AndroidUtils.PrefHelper.remPrefs"), { 21 | {QStringLiteral("id"), _id} 22 | }); 23 | } 24 | 25 | SharedPreferences *SharedPreferences::getPreferences(QObject *parent) 26 | { 27 | auto id = QUuid::createUuid().toString(); 28 | auto prefs = new SharedPreferences(id, parent); 29 | AndroidNative::SystemDispatcher::instance()->dispatch(QStringLiteral("AndroidUtils.PrefHelper.getPrefs"), { 30 | {QStringLiteral("id"), id} 31 | }); 32 | return prefs; 33 | } 34 | 35 | SharedPreferences *SharedPreferences::getSharedPreferences(const QString &name, QObject *parent) 36 | { 37 | auto id = QUuid::createUuid().toString(); 38 | auto prefs = new SharedPreferences(id, parent); 39 | AndroidNative::SystemDispatcher::instance()->dispatch(QStringLiteral("AndroidUtils.PrefHelper.getSharedPrefs"), { 40 | {QStringLiteral("id"), id}, 41 | {QStringLiteral("name"), name} 42 | }); 43 | return prefs; 44 | } 45 | 46 | QStringList SharedPreferences::keys() const 47 | { 48 | return _data.keys(); 49 | } 50 | 51 | bool SharedPreferences::contains(const QString &key) const 52 | { 53 | return _data.contains(key); 54 | } 55 | 56 | QVariant SharedPreferences::value(const QString &key) const 57 | { 58 | return _data.value(key); 59 | } 60 | 61 | QVariantMap SharedPreferences::data() const 62 | { 63 | return _data; 64 | } 65 | 66 | void SharedPreferences::setValue(const QString &key, const QVariant &value) 67 | { 68 | _data.insert(key, value); 69 | AndroidNative::SystemDispatcher::instance()->dispatch(QStringLiteral("AndroidUtils.PrefHelper.save"), { 70 | {QStringLiteral("id"), _id}, 71 | {QStringLiteral("key"), key}, 72 | {QStringLiteral("value"), value} 73 | }); 74 | emit changed(key, value); 75 | } 76 | 77 | void SharedPreferences::remove(const QString &key) 78 | { 79 | _data.remove(key); 80 | AndroidNative::SystemDispatcher::instance()->dispatch(QStringLiteral("AndroidUtils.PrefHelper.remove"), { 81 | {QStringLiteral("id"), _id}, 82 | {QStringLiteral("key"), key} 83 | }); 84 | emit changed(key, QVariant()); 85 | } 86 | 87 | void SharedPreferences::dispatched(const QString &message, const QVariantMap &data) 88 | { 89 | if(message == QStringLiteral("AndroidUtils.PrefHelper.changed.") + _id) { 90 | auto key = data.value(QStringLiteral("key")).toString(); 91 | auto removed = data.value(QStringLiteral("removed"), false).toBool(); 92 | auto value = data.value(QStringLiteral("value")); 93 | if(removed) { 94 | if(_data.remove(key) > 0) 95 | emit changed(key, QVariant()); 96 | } else { 97 | _data.insert(key, value); 98 | emit changed(key, value); 99 | } 100 | } else if(message == QStringLiteral("AndroidUtils.PrefHelper.loaded.") + _id) { 101 | _data = data; 102 | emit loaded(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /sharedpreferences.h: -------------------------------------------------------------------------------- 1 | #ifndef SHAREDPREFERENCES_H 2 | #define SHAREDPREFERENCES_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class SharedPreferences : public QObject 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | ~SharedPreferences(); 14 | 15 | static SharedPreferences *getPreferences(QObject *parent = nullptr); 16 | static SharedPreferences *getSharedPreferences(const QString &name, QObject *parent = nullptr); 17 | 18 | Q_INVOKABLE QStringList keys() const; 19 | Q_INVOKABLE bool contains(const QString &key) const; 20 | Q_INVOKABLE QVariant value(const QString &key) const; 21 | 22 | Q_INVOKABLE QVariantMap data() const; 23 | 24 | public slots: 25 | void setValue(const QString &key, const QVariant &value); 26 | void remove(const QString &key); 27 | 28 | signals: 29 | void loaded(); 30 | void changed(const QString &key, const QVariant &value); 31 | 32 | private slots: 33 | void dispatched(const QString &message, const QVariantMap &data); 34 | 35 | private: 36 | const QString _id; 37 | QVariantMap _data; 38 | 39 | explicit SharedPreferences(const QString &id, QObject *parent = nullptr); 40 | }; 41 | 42 | #endif // SHAREDPREFERENCES_H 43 | --------------------------------------------------------------------------------