diff --git a/CMakeLists.txt b/CMakeLists.txt index f4f5ea4e4..62aa1f4d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,8 @@ include(ECMCheckOutboundLicense) include(ECMQtDeclareLoggingCategory) include(ECMAddAndroidApk) include(ECMQmlModule) +include(GenerateExportHeader) +include(ECMGenerateHeaders) if (NOT ANDROID) include(KDEClangFormat) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 235c81668..e869318b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,6 +194,8 @@ add_library(neochat STATIC neochatroommember.h models/threadmodel.cpp models/threadmodel.h + permissionmanager.cpp + permissionmanager.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES @@ -304,6 +306,10 @@ add_subdirectory(devtools) add_subdirectory(login) add_subdirectory(chatbar) +if (ANDROID) + add_subdirectory(kandroidextras) +endif() + if(NOT ANDROID AND NOT WIN32) qt_target_qml_sources(neochat QML_FILES qml/ShareAction.qml @@ -436,7 +442,7 @@ endif() if(ANDROID) target_sources(neochat PRIVATE notifyrc.qrc) - target_link_libraries(neochat PRIVATE Qt::Svg OpenSSL::SSL) + target_link_libraries(neochat PUBLIC KAndroidExtras PRIVATE Qt::Svg OpenSSL::SSL) if(SQLite3_FOUND) target_link_libraries(neochat-app PRIVATE SQLite::SQLite3) endif() diff --git a/src/kandroidextras/CMakeLists.txt b/src/kandroidextras/CMakeLists.txt new file mode 100644 index 000000000..ebdd08df4 --- /dev/null +++ b/src/kandroidextras/CMakeLists.txt @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2019 Volker Krause +# SPDX-License-Identifier: BSD-3-Clause + +add_library(KAndroidExtras STATIC) + +target_sources(KAndroidExtras PRIVATE + android/activity.cpp + android/android_headers.cpp + android/contentresolver.cpp + android/context.cpp + android/intent.cpp + android/uri.cpp + + java/java_headers.cpp + java/javalocale.cpp + + jni/jni_headers.cpp +) + +if (NOT ANDROID) + target_sources(KAndroidExtras PRIVATE + fake/mock_impl.cpp + fake/mock_jniobject.cpp + ) +endif() + +generate_export_header(KAndroidExtras BASE_NAME KAndroidExtras) +target_compile_features(KAndroidExtras PUBLIC cxx_std_20) # for __VA_OPT__ +target_link_libraries(KAndroidExtras PUBLIC Qt::Core) + +if (ANDROID) + target_link_libraries(KAndroidExtras PRIVATE Qt::CorePrivate) +else() + target_include_directories(KAndroidExtras PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/fake ${CMAKE_CURRENT_SOURCE_DIR}/fake/qt6) +endif() + +ecm_generate_headers(KAndroidExtras_android_FORWARDING_HEADERS + HEADER_NAMES + Activity + AndroidTypes + CalendarContract + ContentResolver + Context + Intent + ManifestPermission + OpenableColumns + Settings + Uri + PREFIX KAndroidExtras + REQUIRED_HEADERS KAndroidExtras_android_HEADERS + RELATIVE android +) + +ecm_generate_headers(KAndroidExtras_java_FORWARDING_HEADERS + HEADER_NAMES + JavaLocale + JavaTypes + PREFIX KAndroidExtras + REQUIRED_HEADERS KAndroidExtras_java_HEADERS + RELATIVE java +) + +ecm_generate_headers(KAndroidExtras_jni_FORWARDING_HEADERS + HEADER_NAMES + JniArgument + JniArray + JniObject + JniMethod + JniReturnValue + JniPp + JniProperty + JniSignature + JniTypes + JniTypeTraits + PREFIX KAndroidExtras + REQUIRED_HEADERS KAndroidExtras_jni_HEADERS + RELATIVE jni +) diff --git a/src/kandroidextras/README.md b/src/kandroidextras/README.md new file mode 100644 index 000000000..2f9c3ebbe --- /dev/null +++ b/src/kandroidextras/README.md @@ -0,0 +1,25 @@ +# KAndroidExtras + +Utilities for using Java Native Interface (JNI) to interface with Android platform API. + +## Java Native Interface (JNI) wrapper + +C++ header-only code for defining compile-time checked JNI wrappers. + +Supported: +- typed `jobject` wrappers (`Jni::Object`) +- wrappers for Java arrays holding primitive or non-primitive content (`Jni::Array`) +- reading static final properties (`JNI_CONSTANT`) +- reading and writing non-static properties (`JNI_PROPERTY`) +- static and non-static method calls, constructors (`JNI_METHOD`, `JNI_STATIC_METHOD`, `JNI_CONSTRUCTOR`) + +Not yet supported: +- registering native methods for Java -> C++ calls + +## JNI mock implementation + +This is useful for automated testing of JNI code on other platforms than Android. + +## Wrappers for Java and Android types + +Predefined wrappers for common platform types needed in multiple places. diff --git a/src/kandroidextras/README.md.license b/src/kandroidextras/README.md.license new file mode 100644 index 000000000..323fa40d6 --- /dev/null +++ b/src/kandroidextras/README.md.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2022 Volker Krause +SPDX-License-Identifier: CC0-1.0 diff --git a/src/kandroidextras/android/activity.cpp b/src/kandroidextras/android/activity.cpp new file mode 100644 index 000000000..90c8bdd88 --- /dev/null +++ b/src/kandroidextras/android/activity.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "activity.h" + +#include +#include +#include + +#include "private/qandroidextras_p.h" +#include +#include + +using namespace KAndroidExtras; + +Intent Activity::getIntent() +{ + const QJniObject activity = QNativeInterface::QAndroidApplication::context(); + if (!activity.isValid()) + return {}; + + const auto intent = activity.callObjectMethod("getIntent", Jni::signature()); + return Intent(Jni::fromHandle(intent)); +} + +bool Activity::startActivity(const Intent &intent, int receiverRequestCode) +{ + QJniEnvironment jniEnv; + QtAndroidPrivate::startActivity(intent, receiverRequestCode); + if (jniEnv->ExceptionCheck()) { + jniEnv->ExceptionClear(); + return false; + } + return true; +} diff --git a/src/kandroidextras/android/activity.h b/src/kandroidextras/android/activity.h new file mode 100644 index 000000000..3cfbbccff --- /dev/null +++ b/src/kandroidextras/android/activity.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_ACTIVITY_H +#define KANDROIDEXTRAS_ACTIVITY_H + +#include "kandroidextras_export.h" + +namespace KAndroidExtras +{ + +class Intent; + +/** Methods around android.app.Activity. */ +namespace Activity +{ +/** Returns the Intent that started the activity. */ +KANDROIDEXTRAS_EXPORT Intent getIntent(); + +/** Same as QtAndroid::startActivity(), but with exception handling. */ +KANDROIDEXTRAS_EXPORT bool startActivity(const Intent &intent, int receiverRequestCode); // TODO add callback arg +} + +} + +#endif // KANDROIDEXTRAS_ACTIVITY_H diff --git a/src/kandroidextras/android/android_headers.cpp b/src/kandroidextras/android/android_headers.cpp new file mode 100644 index 000000000..e4ae0368c --- /dev/null +++ b/src/kandroidextras/android/android_headers.cpp @@ -0,0 +1,13 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +// list all headers here that have no .cpp file +// this only serves as a guarantee that these headers actually compile +#include "androidtypes.h" +#include "calendarcontract.h" +#include "manifestpermission.h" +#include "openablecolumns.h" +#include "settings.h" diff --git a/src/kandroidextras/android/androidtypes.h b/src/kandroidextras/android/androidtypes.h new file mode 100644 index 000000000..7be54986e --- /dev/null +++ b/src/kandroidextras/android/androidtypes.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_ANDROIDTYPES_H +#define KANDROIDEXTRAS_ANDROIDTYPES_H + +#include + +namespace KAndroidExtras +{ + +JNI_TYPE(android, content, ContentResolver) +JNI_TYPE(android, content, Context) +JNI_TYPE(android, content, Intent) +JNI_TYPE(android, database, Cursor) +JNI_NESTED_TYPE(android, Manifest, permission) +JNI_TYPE(android, net, Uri) +JNI_NESTED_TYPE(android, provider, CalendarContract, AttendeesColumns) +JNI_NESTED_TYPE(android, provider, CalendarContract, CalendarColumns) +JNI_NESTED_TYPE(android, provider, CalendarContract, EventsColumns) +JNI_NESTED_TYPE(android, provider, CalendarContract, RemindersColumns) +JNI_TYPE(android, provider, OpenableColumns) +JNI_TYPE(android, provider, Settings) + +} + +#endif // KANDROIDEXTRAS_ANDROIDTYPES_H diff --git a/src/kandroidextras/android/calendarcontract.h b/src/kandroidextras/android/calendarcontract.h new file mode 100644 index 000000000..54298fbaf --- /dev/null +++ b/src/kandroidextras/android/calendarcontract.h @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_CALENDARCONTRACT_H +#define KANDROIDEXTRAS_CALENDARCONTRACT_H + +#include +#include +#include +#include + +namespace KAndroidExtras +{ + +/** CalendarContracts.EventColumns wrapper. */ +class CalendarColumns +{ + JNI_UNMANAGED_OBJECT(CalendarColumns, android::provider::CalendarContract_CalendarColumns) + + JNI_CONSTANT(jint, CAL_ACCESS_CONTRIBUTOR) + JNI_CONSTANT(jint, CAL_ACCESS_EDITOR) + JNI_CONSTANT(jint, CAL_ACCESS_FREEBUSY) + JNI_CONSTANT(jint, CAL_ACCESS_NONE) + JNI_CONSTANT(jint, CAL_ACCESS_OVERRIDE) + JNI_CONSTANT(jint, CAL_ACCESS_OWNER) + JNI_CONSTANT(jint, CAL_ACCESS_READ) + JNI_CONSTANT(jint, CAL_ACCESS_RESPOND) + JNI_CONSTANT(jint, CAL_ACCESS_ROOT) +}; + +/** CalendarContracts.EventColumns wrapper. */ +class EventsColumns +{ + JNI_UNMANAGED_OBJECT(EventsColumns, android::provider::CalendarContract_EventsColumns) + + JNI_CONSTANT(jint, ACCESS_CONFIDENTIAL) + JNI_CONSTANT(jint, ACCESS_DEFAULT) + JNI_CONSTANT(jint, ACCESS_PRIVATE) + JNI_CONSTANT(jint, ACCESS_PUBLIC) + + JNI_CONSTANT(jint, AVAILABILITY_BUSY) + JNI_CONSTANT(jint, AVAILABILITY_FREE) + JNI_CONSTANT(jint, AVAILABILITY_TENTATIVE) +}; + +/** CalendarContracts.AttendeesColumns wrapper. */ +class AttendeesColumns +{ + JNI_UNMANAGED_OBJECT(AttendeesColumns, android::provider::CalendarContract_AttendeesColumns) + + JNI_CONSTANT(jint, ATTENDEE_STATUS_ACCEPTED) + JNI_CONSTANT(jint, ATTENDEE_STATUS_DECLINED) + JNI_CONSTANT(jint, ATTENDEE_STATUS_INVITED) + JNI_CONSTANT(jint, ATTENDEE_STATUS_NONE) + JNI_CONSTANT(jint, ATTENDEE_STATUS_TENTATIVE) + + JNI_CONSTANT(jint, RELATIONSHIP_ATTENDEE) + JNI_CONSTANT(jint, RELATIONSHIP_NONE) + JNI_CONSTANT(jint, RELATIONSHIP_ORGANIZER) + JNI_CONSTANT(jint, RELATIONSHIP_PERFORMER) + JNI_CONSTANT(jint, RELATIONSHIP_SPEAKER) + + JNI_CONSTANT(jint, TYPE_NONE) + JNI_CONSTANT(jint, TYPE_OPTIONAL) + JNI_CONSTANT(jint, TYPE_REQUIRED) + JNI_CONSTANT(jint, TYPE_RESOURCE) +}; + +/** CalendarContract.RemindersColumns wrapper. */ +class RemindersColumns +{ + JNI_UNMANAGED_OBJECT(RemindersColumns, android::provider::CalendarContract_RemindersColumns) + + JNI_CONSTANT(jint, METHOD_ALARM) + JNI_CONSTANT(jint, METHOD_ALERT) + JNI_CONSTANT(jint, METHOD_DEFAULT) + JNI_CONSTANT(jint, METHOD_EMAIL) + JNI_CONSTANT(jint, METHOD_SMS) +}; + +} + +#endif // KANDROIDEXTRAS_OPENABLECOLUMNS_H diff --git a/src/kandroidextras/android/contentresolver.cpp b/src/kandroidextras/android/contentresolver.cpp new file mode 100644 index 000000000..fc5569a82 --- /dev/null +++ b/src/kandroidextras/android/contentresolver.cpp @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "contentresolver.h" +#include "openablecolumns.h" +#include "uri.h" + +#include +#include + +#include +#include +#include + +using namespace KAndroidExtras; + +QJniObject ContentResolver::get() +{ + const QJniObject context = QNativeInterface::QAndroidApplication::context(); + return context.callObjectMethod("getContentResolver", Jni::signature()); +} + +QString ContentResolver::mimeType(const QUrl &url) +{ + auto cs = ContentResolver::get(); + const auto uri = Uri::fromUrl(url); + auto mt = cs.callObjectMethod("getType", Jni::signature(), uri.object()); + return mt.toString(); +} + +QString ContentResolver::fileName(const QUrl &url) +{ + auto cs = ContentResolver::get(); + const auto uri = Uri::fromUrl(url); + + // query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) + auto cursor = cs.callObjectMethod( + "query", + Jni::signature(), + uri.object(), + 0, + 0, + 0, + 0); + + const QJniObject DISPLAY_NAME = OpenableColumns::DISPLAY_NAME; + const auto nameIndex = cursor.callMethod("getColumnIndex", (const char *)Jni::signature(), DISPLAY_NAME.object()); + cursor.callMethod("moveToFirst", (const char *)Jni::signature()); + return cursor.callObjectMethod("getString", Jni::signature(), nameIndex).toString(); +} diff --git a/src/kandroidextras/android/contentresolver.h b/src/kandroidextras/android/contentresolver.h new file mode 100644 index 000000000..c54bdceb0 --- /dev/null +++ b/src/kandroidextras/android/contentresolver.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_CONTENTRESOLVER_H +#define KANDROIDEXTRAS_CONTENTRESOLVER_H + +#include "kandroidextras_export.h" + +#include +class QJniObject; +class QString; +class QUrl; + +namespace KAndroidExtras +{ + +/** Methods for working with Android's ContentResolver. */ +namespace ContentResolver +{ +/** Get the JNI content resolver. */ +KANDROIDEXTRAS_EXPORT QJniObject get(); + +/** Returns the mime type of the given content: URL. + * @see Android ContentResolver.getType + */ +KANDROIDEXTRAS_EXPORT QString mimeType(const QUrl &url); + +/** File name of a file provided by a content: URL. */ +KANDROIDEXTRAS_EXPORT QString fileName(const QUrl &url); +} + +} + +#endif // KANDROIDEXTRAS_CONTENTRESOLVER_H diff --git a/src/kandroidextras/android/context.cpp b/src/kandroidextras/android/context.cpp new file mode 100644 index 000000000..c7d1db911 --- /dev/null +++ b/src/kandroidextras/android/context.cpp @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "context.h" + +#include +#include + +#include + +using namespace KAndroidExtras; + +QJniObject Context::getPackageName() +{ + const QJniObject context = QNativeInterface::QAndroidApplication::context(); + return context.callObjectMethod("getPackageName", Jni::signature()); +} diff --git a/src/kandroidextras/android/context.h b/src/kandroidextras/android/context.h new file mode 100644 index 000000000..194f87885 --- /dev/null +++ b/src/kandroidextras/android/context.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_CONTEXT_H +#define KANDROIDEXTRAS_CONTEXT_H + +#include "kandroidextras_export.h" + +class QJniObject; + +namespace KAndroidExtras +{ + +/** Methods around android.content.Context. */ +namespace Context +{ +KANDROIDEXTRAS_EXPORT QJniObject getPackageName(); +} + +} + +#endif // KANDROIDEXTRAS_CONTEXT_H diff --git a/src/kandroidextras/android/intent.cpp b/src/kandroidextras/android/intent.cpp new file mode 100644 index 000000000..89652b6e1 --- /dev/null +++ b/src/kandroidextras/android/intent.cpp @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "intent.h" +#include "uri.h" + +#include +#include + +#include + +using namespace KAndroidExtras; + +Intent::~Intent() = default; + +Intent::operator QJniObject() const +{ + return jniHandle(); +} + +template +QJniObject Intent::getObjectExtra(const char *methodName, const QJniObject &name) const +{ + return jniHandle().callObjectMethod(methodName, Jni::signature(), name.object()); +} + +QString Intent::getStringExtra(const QJniObject &name) const +{ + return getObjectExtra("getStringExtra", name).toString(); +} + +QStringList Intent::getStringArrayExtra(const QJniObject &name) const +{ + const auto extra = getObjectExtra>("getStringArrayExtra", name); + return Jni::fromArray(extra); +} diff --git a/src/kandroidextras/android/intent.h b/src/kandroidextras/android/intent.h new file mode 100644 index 000000000..fc87d0e3f --- /dev/null +++ b/src/kandroidextras/android/intent.h @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_INTENT_H +#define KANDROIDEXTRAS_INTENT_H + +#include "kandroidextras_export.h" + +#include +#include +#include +#include +#include + +class QUrl; + +namespace KAndroidExtras +{ + +/** Methods to interact with android.content.Intent objects. + * This does not only offer features beyond what QAndroidIntent, it also provides + * a putExtra() implementation that actually interoperates with system services. + */ +class KANDROIDEXTRAS_EXPORT Intent +{ + JNI_OBJECT(Intent, android::content::Intent) +public: + /** Creates a new empty intent. */ + JNI_CONSTRUCTOR(Intent) + ~Intent(); + + /** Add a category to the intent. */ + JNI_METHOD(android::content::Intent, addCategory, java::lang::String) + /** Add flags to this intent. */ + JNI_METHOD(android::content::Intent, addFlags, jint) + /** Returns the data of this intent. */ + JNI_METHOD(android::net::Uri, getData) + /** Get the intent action. */ + JNI_METHOD(java::lang::String, getAction) + /** Sets the action of the intent. */ + JNI_METHOD(android::content::Intent, setAction, java::lang::String) + /** Set the data URL of this intent. */ + JNI_METHOD(android::content::Intent, setData, android::net::Uri) + + /** Returns the mimetype of this intent. */ + JNI_METHOD(java::lang::String, getType) + /** Set the mime type for this intent. */ + JNI_METHOD(android::content::Intent, setType, java::lang::String) + + /** Read extra intent data. */ + QString getStringExtra(const QJniObject &name) const; + QStringList getStringArrayExtra(const QJniObject &name) const; + /** Add extra intent data of type @tparam T. */ + template + inline void putExtra(const QJniObject &name, const QJniObject &value) + { + jniHandle().callObjectMethod("putExtra", Jni::signature(), name.object(), value.object()); + } + + /** Implicit conversion to an QJniObject. */ + operator QJniObject() const; + + /** Action constant for create document intents. */ + JNI_CONSTANT(java::lang::String, ACTION_CREATE_DOCUMENT) + /** Main activity entry point. */ + JNI_CONSTANT(java::lang::String, ACTION_MAIN) + /** Action constant for open document intents. */ + JNI_CONSTANT(java::lang::String, ACTION_OPEN_DOCUMENT) + /** Action constant for viewing intents. */ + JNI_CONSTANT(java::lang::String, ACTION_VIEW) + /** Share data. */ + JNI_CONSTANT(java::lang::String, ACTION_SEND) + /** Share multiple data items. */ + JNI_CONSTANT(java::lang::String, ACTION_SEND_MULTIPLE) + + /** Category constant for openable content. */ + JNI_CONSTANT(java::lang::String, CATEGORY_OPENABLE) + + JNI_CONSTANT(java::lang::String, EXTRA_EMAIL) + JNI_CONSTANT(java::lang::String, EXTRA_STREAM) + JNI_CONSTANT(java::lang::String, EXTRA_SUBJECT) + JNI_CONSTANT(java::lang::String, EXTRA_TEXT) + + /** Flag for granting read URI permissions on content providers. */ + JNI_CONSTANT(jint, FLAG_GRANT_READ_URI_PERMISSION) + /** Flag for granting write URI permissions on content providers. */ + JNI_CONSTANT(jint, FLAG_GRANT_WRITE_URI_PERMISSION) + +private: + template + QJniObject getObjectExtra(const char *methodName, const QJniObject &name) const; +}; + +} + +#endif // KANDROIDEXTRAS_INTENT_H diff --git a/src/kandroidextras/android/manifestpermission.h b/src/kandroidextras/android/manifestpermission.h new file mode 100644 index 000000000..a24f6f9bb --- /dev/null +++ b/src/kandroidextras/android/manifestpermission.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MANIFESTPERMISSIONM_H +#define KANDROIDEXTRAS_MANIFESTPERMISSIONM_H + +#include +#include +#include +#include + +namespace KAndroidExtras +{ + +/** + * Access to manifest permissions. + * @see https://developer.android.com/reference/android/Manifest.permission + */ +class ManifestPermission +{ + JNI_UNMANAGED_OBJECT(ManifestPermission, android::Manifest_permission) +public: + JNI_CONSTANT(java::lang::String, READ_CALENDAR) + JNI_CONSTANT(java::lang::String, WRITE_CALENDAR) + JNI_CONSTANT(java::lang::String, READ_EXTERNAL_STORAGE) + JNI_CONSTANT(java::lang::String, WRITE_EXTERNAL_STORAGE) + JNI_CONSTANT(java::lang::String, POST_NOTIFICATIONS) + JNI_CONSTANT(java::lang::String, CAMERA) +}; + +} + +#endif // KANDROIDEXTRAS_MANIFESTPERMISSIONM_H diff --git a/src/kandroidextras/android/openablecolumns.h b/src/kandroidextras/android/openablecolumns.h new file mode 100644 index 000000000..27e853ab5 --- /dev/null +++ b/src/kandroidextras/android/openablecolumns.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_OPENABLECOLUMNS_H +#define KANDROIDEXTRAS_OPENABLECOLUMNS_H + +#include +#include +#include +#include + +namespace KAndroidExtras +{ + +/** + * Constants for ContentResolver queries. + * @see https://developer.android.com/reference/android/provider/OpenableColumns + */ +class OpenableColumns +{ + JNI_UNMANAGED_OBJECT(OpenableColumns, android::provider::OpenableColumns) +public: + JNI_CONSTANT(java::lang::String, DISPLAY_NAME) + JNI_CONSTANT(java::lang::String, SIZE) +}; + +} + +#endif // KANDROIDEXTRAS_OPENABLECOLUMNS_H diff --git a/src/kandroidextras/android/settings.h b/src/kandroidextras/android/settings.h new file mode 100644 index 000000000..8319bcc05 --- /dev/null +++ b/src/kandroidextras/android/settings.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_SETTINGS_H +#define KANDROIDEXTRAS_SETTINGS_H + +#include +#include + +namespace KAndroidExtras +{ + +/** Methods around android.provider.Settings. */ +class Settings +{ + JNI_UNMANAGED_OBJECT(Settings, android::provider::Settings) + JNI_CONSTANT(java::lang::String, ACTION_APP_NOTIFICATION_SETTINGS) + JNI_CONSTANT(java::lang::String, ACTION_CHANNEL_NOTIFICATION_SETTINGS) + JNI_CONSTANT(java::lang::String, EXTRA_APP_PACKAGE) + JNI_CONSTANT(java::lang::String, EXTRA_CHANNEL_ID) +}; + +} + +#endif // KANDROIDEXTRAS_SETTINGS_H diff --git a/src/kandroidextras/android/uri.cpp b/src/kandroidextras/android/uri.cpp new file mode 100644 index 000000000..b689b1a5b --- /dev/null +++ b/src/kandroidextras/android/uri.cpp @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "uri.h" + +#include +#include + +using namespace KAndroidExtras; + +QJniObject Uri::fromUrl(const QUrl &url) +{ + return QJniObject::callStaticObjectMethod(Jni::typeName(), + "parse", + Jni::signature(), + QJniObject::fromString(url.toString(QUrl::FullyEncoded)).object()); +} + +QUrl Uri::toUrl(const QJniObject &uri) +{ + if (!uri.isValid()) { + return QUrl(); + } + return QUrl(uri.callObjectMethod("toString", Jni::signature()).toString()); +} diff --git a/src/kandroidextras/android/uri.h b/src/kandroidextras/android/uri.h new file mode 100644 index 000000000..9cd9a5874 --- /dev/null +++ b/src/kandroidextras/android/uri.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_URI_H +#define KANDROIDEXTRAS_URI_H + +#include "kandroidextras_export.h" + +#include +#include + +#include + +namespace KAndroidExtras +{ + +/** Conversion methods for android.net.Uri. */ +namespace Uri +{ +/** Create an android.net.Uri from a QUrl. */ +KANDROIDEXTRAS_EXPORT QJniObject fromUrl(const QUrl &url); + +/** Convert a android.net.Uri to a QUrl. */ +KANDROIDEXTRAS_EXPORT QUrl toUrl(const QJniObject &uri); +} + +JNI_DECLARE_CONVERTER(android::net::Uri, QUrl, (Uri::toUrl(value)), (Uri::fromUrl(value))) + +} + +#endif // KANDROIDEXTRAS_URI_H diff --git a/src/kandroidextras/fake/jni.h b/src/kandroidextras/fake/jni.h new file mode 100644 index 000000000..6bbd1ab4d --- /dev/null +++ b/src/kandroidextras/fake/jni.h @@ -0,0 +1,208 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef FAKE_JNI_H +#define FAKE_JNI_H + +#include +#include + +#ifdef Q_OS_ANDROID +#error This is a mock object for use on non-Android! +#endif + +typedef uint8_t jboolean; +typedef int8_t jbyte; +typedef uint16_t jchar; +typedef int16_t jshort; +typedef int32_t jint; +typedef int64_t jlong; +typedef float jfloat; +typedef double jdouble; + +typedef jint jsize; + +typedef void *jobject; +typedef jobject jclass; +typedef jobject jstring; +typedef jobject jarray; +typedef jarray jobjectArray; +typedef jarray jbooleanArray; +typedef jarray jbyteArray; +typedef jarray jcharArray; +typedef jarray jshortArray; +typedef jarray jintArray; +typedef jarray jlongArray; +typedef jarray jfloatArray; +typedef jarray jdoubleArray; + +namespace detail +{ +template +inline T *getArrayElements(jsize size) +{ + T *array = new T[size]; + std::iota(array, array + size, T{}); + return array; +} +} + +struct JNIEnv { + inline bool ExceptionCheck() + { + return false; + } + inline void ExceptionClear() + { + } + + inline int GetArrayLength(jobjectArray) + { + return m_arrayLength; + } + inline jobjectArray NewObjectArray(jsize, jclass, jobject) + { + return nullptr; + } + inline jobject GetObjectArrayElement(jobjectArray, int index) + { + return reinterpret_cast(index); + } + inline void SetObjectArrayElement(jobjectArray, jsize, jobject) + { + } + + inline jbooleanArray NewBooleanArray(jsize) + { + return nullptr; + } + inline jbyteArray NewByteArray(jsize) + { + return nullptr; + } + inline jcharArray NewCharArray(jsize) + { + return nullptr; + } + inline jshortArray NewShortArray(jsize) + { + return nullptr; + } + inline jintArray NewIntArray(jsize) + { + return nullptr; + } + inline jlongArray NewLongArray(jsize) + { + return nullptr; + } + inline jfloatArray NewFloatArray(jsize) + { + return nullptr; + } + inline jdoubleArray NewDoubleArray(jsize) + { + return nullptr; + } + + inline jboolean *GetBooleanArrayElements(jbooleanArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jbyte *GetByteArrayElements(jbyteArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jchar *GetCharArrayElements(jcharArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jshort *GetShortArrayElements(jshortArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jint *GetIntArrayElements(jintArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jlong *GetLongArrayElements(jlongArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jfloat *GetFloatArrayElements(jfloatArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + inline jdouble *GetDoubleArrayElements(jdoubleArray, jboolean *) + { + return detail::getArrayElements(m_arrayLength); + } + + inline void ReleaseBooleanArrayElements(jbooleanArray, jboolean *data, jint) + { + delete[] data; + } + inline void ReleaseByteArrayElements(jbyteArray, jbyte *data, jint) + { + delete[] data; + } + inline void ReleaseCharArrayElements(jcharArray, jchar *data, jint) + { + delete[] data; + } + inline void ReleaseShortArrayElements(jshortArray, jshort *data, jint) + { + delete[] data; + } + inline void ReleaseIntArrayElements(jintArray, jint *data, jint) + { + delete[] data; + } + inline void ReleaseLongArrayElements(jlongArray, jlong *data, jint) + { + delete[] data; + } + inline void ReleaseFloatArrayElements(jfloatArray, jfloat *data, jint) + { + delete[] data; + } + inline void ReleaseDoubleArrayElements(jdoubleArray, jdouble *data, jint) + { + delete[] data; + } + + inline void SetBooleanArrayRegion(jbooleanArray, jsize, jsize, const jboolean *) + { + } + inline void SetByteArrayRegion(jbyteArray, jsize, jsize, const jbyte *) + { + } + inline void SetCharArrayRegion(jcharArray, jsize, jsize, const jchar *) + { + } + inline void SetShortArrayRegion(jshortArray, jsize, jsize, const jshort *) + { + } + inline void SetIntArrayRegion(jintArray, jsize, jsize, const jint *) + { + } + inline void SetLongArrayRegion(jlongArray, jsize, jsize, const jlong *) + { + } + inline void SetFloatArrayRegion(jfloatArray, jsize, jsize, const jfloat *) + { + } + inline void SetDoubleArrayRegion(jdoubleArray, jsize, jsize, const jdouble *) + { + } + + static jsize m_arrayLength; +}; + +#define JNI_COMMIT 1 +#define JNI_ABORT 2 + +#endif diff --git a/src/kandroidextras/fake/mock_impl.cpp b/src/kandroidextras/fake/mock_impl.cpp new file mode 100644 index 000000000..834404d07 --- /dev/null +++ b/src/kandroidextras/fake/mock_impl.cpp @@ -0,0 +1,9 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "jni.h" + +int JNIEnv::m_arrayLength = 0; diff --git a/src/kandroidextras/fake/mock_jnienvironment.h b/src/kandroidextras/fake/mock_jnienvironment.h new file mode 100644 index 000000000..b5ae85923 --- /dev/null +++ b/src/kandroidextras/fake/mock_jnienvironment.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MOCK_JNIENVIRONMENT_H +#define KANDROIDEXTRAS_MOCK_JNIENVIRONMENT_H + +#include "jni.h" + +namespace KAndroidExtras +{ +class MockJniEnvironment +{ +public: + inline jclass findClass(const char *) + { + return nullptr; + } + inline JNIEnv *operator->() + { + return &m_env; + } + +protected: + mutable JNIEnv m_env; +}; +} + +#endif diff --git a/src/kandroidextras/fake/mock_jniobject.cpp b/src/kandroidextras/fake/mock_jniobject.cpp new file mode 100644 index 000000000..cf14e5749 --- /dev/null +++ b/src/kandroidextras/fake/mock_jniobject.cpp @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "mock_jniobject.h" + +using namespace KAndroidExtras::Internal; + +QStringList MockJniObjectBase::m_staticProtocol; + +class MockJniObjectBasePrivate : public QSharedData +{ +public: + QStringList protocol; + QHash properties; + QVariant value; +}; + +MockJniObjectBase::MockJniObjectBase() + : d(new MockJniObjectBasePrivate) +{ +} + +MockJniObjectBase::MockJniObjectBase(const char *className) + : d(new MockJniObjectBasePrivate) +{ + addToProtocol(QLatin1StringView("ctor: ") + QLatin1StringView(className)); +} + +MockJniObjectBase::MockJniObjectBase(jobject object) + : d(new MockJniObjectBasePrivate) +{ + Q_UNUSED(object); + addToProtocol(QLatin1StringView("ctor: o")); +} + +MockJniObjectBase::MockJniObjectBase(const MockJniObjectBase &) = default; +MockJniObjectBase &MockJniObjectBase::operator=(const MockJniObjectBase &) = default; +MockJniObjectBase::~MockJniObjectBase() = default; + +QStringList MockJniObjectBase::protocol() const +{ + return d->protocol; +} + +void MockJniObjectBase::addToProtocol(const QString &line) const +{ + d->protocol.push_back(line); +} + +void MockJniObjectBase::setProtocol(const QStringList &protocol) +{ + d->protocol = protocol; +} + +QVariant MockJniObjectBase::property(const QByteArray &name) const +{ + return d->properties.value(name); +} + +void MockJniObjectBase::setProperty(const QByteArray &name, const QVariant &value) +{ + d->properties.insert(name, value); +} + +QVariant MockJniObjectBase::value() const +{ + return d->value; +} + +void MockJniObjectBase::setValue(const QVariant &value) +{ + d->value = value; +} + +void MockJniObjectBase::setData(jobject object) +{ + d = reinterpret_cast(object); +} diff --git a/src/kandroidextras/fake/mock_jniobject.h b/src/kandroidextras/fake/mock_jniobject.h new file mode 100644 index 000000000..3c808c332 --- /dev/null +++ b/src/kandroidextras/fake/mock_jniobject.h @@ -0,0 +1,262 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MOCK_JNIOBJECT_H +#define KANDROIDEXTRAS_MOCK_JNIOBJECT_H + +#include "jni.h" +#include "kandroidextras_export.h" + +#include "kandroidextras/jnisignature.h" +#include "kandroidextras/jnitypes.h" + +#include +#include +#include + +class MockJniObjectBasePrivate; + +namespace KAndroidExtras +{ +namespace Internal +{ +template +constexpr inline const char *argTypeToString() +{ + return KAndroidExtras::Jni::signature(); +} +template<> +constexpr inline const char *argTypeToString() +{ + return "o"; +} + +class KANDROIDEXTRAS_EXPORT MockJniObjectBase +{ +public: + MockJniObjectBase(); + MockJniObjectBase(const MockJniObjectBase &); + ~MockJniObjectBase(); + MockJniObjectBase &operator=(const MockJniObjectBase &); + + MockJniObjectBase(const char *className); + + inline MockJniObjectBase(const char *className, const char *signature) + : MockJniObjectBase() + { + addToProtocol(QLatin1StringView("ctor: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(signature)); + } + template + inline MockJniObjectBase(const char *className, const char *signature, Args...) + : MockJniObjectBase() + { + addToProtocol(QLatin1StringView("ctor: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(signature) + QLatin1StringView(" (") + + (... + QLatin1StringView(KAndroidExtras::Internal::argTypeToString())) + QLatin1Char(')')); + } + + MockJniObjectBase(jobject object); + + bool isValid() const + { + return true; + } + + static QStringList m_staticProtocol; + + QStringList protocol() const; + void addToProtocol(const QString &line) const; + +protected: + QVariant property(const QByteArray &name) const; + void setProperty(const QByteArray &name, const QVariant &value); + QVariant value() const; + void setValue(const QVariant &value); + void setData(jobject object); + + void setProtocol(const QStringList &protocol); + QExplicitlySharedDataPointer d; +}; + +template +class MockJniObject : public MockJniObjectBase +{ +public: + using MockJniObjectBase::MockJniObjectBase; + static inline JniObjectT fromString(const QString &s) + { + JniObjectT o; + o.setValue(s); + return o; + } + static inline JniObjectT fromLocalRef(jobject o) + { + JniObjectT obj; + obj.addToProtocol(QLatin1StringView("ctor: ") + QString::number((qulonglong)o)); + return obj; + } + inline QString toString() const + { + return value().userType() == QMetaType::QString ? value().toString() : protocol().join(QLatin1Char('\n')); + } + + inline jobject object() const + { + return d.data(); + } + template + inline T object() const + { + return {}; + } + + template + inline T callMethod(const char *methodName, const char *signature, Args...) const + { + const QString s = QLatin1StringView("callMethod: ") + QLatin1StringView(methodName) + QLatin1Char(' ') + QLatin1StringView(signature) + + QLatin1StringView(" (") + (... + QLatin1StringView(KAndroidExtras::Internal::argTypeToString())) + QLatin1Char(')'); + addToProtocol(s); + if constexpr (!std::is_same_v) { + return {}; + } + } + template + inline T callMethod(const char *methodName, const char *signature) const + { + const QString s = + QLatin1StringView("callMethod: ") + QLatin1StringView(methodName) + QLatin1Char(' ') + QLatin1StringView(signature) + QLatin1StringView(" ()"); + addToProtocol(s); + if constexpr (!std::is_same_v) { + return {}; + } + } + + template + inline JniObjectT callObjectMethod(const char *methodName, const char *signature, Args...) const + { + const QString s = QLatin1StringView("callObjectMethod: ") + QLatin1StringView(methodName) + QLatin1Char(' ') + QLatin1StringView(signature) + + QLatin1StringView(" (") + (... + QLatin1StringView(KAndroidExtras::Internal::argTypeToString())) + QLatin1Char(')'); + addToProtocol(s); + + JniObjectT obj; + obj.setProtocol(protocol()); + return obj; + } + inline JniObjectT callObjectMethod(const char *methodName, const char *signature) const + { + addToProtocol(QLatin1StringView("callObjectMethod: ") + QLatin1StringView(methodName) + QLatin1Char(' ') + QLatin1StringView(signature) + + QLatin1StringView(" ()")); + + JniObjectT obj; + obj.setProtocol(protocol()); + return obj; + } + + template + inline T getField(const char *fieldName) const + { + addToProtocol(QLatin1StringView("getField: ") + QLatin1StringView(fieldName) + QLatin1Char(' ') + + QLatin1StringView(KAndroidExtras::Jni::signature())); + return property(fieldName).template value(); + } + + inline JniObjectT getObjectField(const char *fieldName, const char *signature) const + { + addToProtocol(QLatin1StringView("getObjectField: ") + QLatin1StringView(fieldName) + QLatin1Char(' ') + QLatin1StringView(signature)); + return property(fieldName).template value(); + } + + template + inline void setField(const char *fieldName, const char *signature, T value) + { + Q_UNUSED(value); + addToProtocol(QLatin1StringView("setField: ") + QLatin1StringView(fieldName) + QLatin1Char(' ') + QLatin1StringView(signature)); + if constexpr (std::is_same_v) { + setObjectProperty(fieldName, value); + } else { + setProperty(fieldName, QVariant::fromValue(value)); + } + } + template + inline void setField(const char *fieldName, T value) + { + setField(fieldName, KAndroidExtras::Jni::signature(), value); + } + + template + static inline T callStaticMethod(const char *className, const char *methodName, const char *signature, Args...) + { + const QString s = QLatin1StringView("callStaticMethod: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(methodName) + + QLatin1Char(' ') + QLatin1StringView(signature) + QLatin1StringView(" (") + + (... + QLatin1StringView(KAndroidExtras::Internal::argTypeToString())) + QLatin1Char(')'); + m_staticProtocol.push_back(s); + if constexpr (!std::is_same_v) { + return {}; + } + } + template + static inline T callStaticMethod(const char *className, const char *methodName, const char *signature) + { + const QString s = QLatin1StringView("callStaticMethod: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(methodName) + + QLatin1Char(' ') + QLatin1StringView(signature) + QLatin1StringView(" ()"); + m_staticProtocol.push_back(s); + if constexpr (!std::is_same_v) { + return {}; + } + } + + template + static inline JniObjectT callStaticObjectMethod(const char *className, const char *methodName, const char *signature, Args...) + { + const QString s = QLatin1StringView("callStaticObjectMethod: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(methodName) + + QLatin1Char(' ') + QLatin1StringView(signature) + QLatin1StringView(" (") + + (... + QLatin1StringView(KAndroidExtras::Internal::argTypeToString())) + QLatin1Char(')'); + JniObjectT obj; + obj.addToProtocol(s); + return obj; + } + + static inline JniObjectT callStaticObjectMethod(const char *className, const char *methodName, const char *signature) + { + JniObjectT obj; + obj.addToProtocol(QLatin1StringView("callStaticObjectMethod: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(methodName) + + QLatin1Char(' ') + QLatin1StringView(signature) + QLatin1StringView(" ()")); + return obj; + } + + static inline JniObjectT getStaticObjectField(const char *className, const char *fieldName, const char *signature) + { + m_staticProtocol.push_back(QLatin1StringView("getStaticObjectField: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(fieldName) + + QLatin1Char(' ') + QLatin1StringView(signature)); + return {}; + } + + template + static inline JniObjectT getStaticObjectField(const char *className, const char *fieldName) + { + m_staticProtocol.push_back(QLatin1StringView("getStaticObjectField<>: ") + QLatin1StringView(className) + QLatin1Char(' ') + + QLatin1StringView(fieldName)); + return {}; + } + + template + static inline T getStaticField(const char *className, const char *fieldName) + { + m_staticProtocol.push_back(QLatin1StringView("getStaticField<>: ") + QLatin1StringView(className) + QLatin1Char(' ') + QLatin1StringView(fieldName) + + QLatin1Char(' ') + QLatin1StringView(KAndroidExtras::Jni::signature())); + return {}; + } + + inline void setObjectProperty(const QByteArray &name, jobject value) + { + JniObjectT o; + o.setData(value); + setProperty(name, QVariant::fromValue(o)); + } +}; + +} +} + +#endif diff --git a/src/kandroidextras/fake/qt6/QCoreApplication b/src/kandroidextras/fake/qt6/QCoreApplication new file mode 100644 index 000000000..511de7168 --- /dev/null +++ b/src/kandroidextras/fake/qt6/QCoreApplication @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MOCK_QCOREAPPLICATION_H +#define KANDROIDEXTRAS_MOCK_QCOREAPPLICATION_H + +#include_next + +#include + +namespace QNativeInterface { +namespace QAndroidApplication { + inline QJniObject context() + { + QJniObject obj; + obj.addToProtocol(QStringLiteral("global androidContext()")); + return obj; + } +} +} + +#endif diff --git a/src/kandroidextras/fake/qt6/QJniEnvironment b/src/kandroidextras/fake/qt6/QJniEnvironment new file mode 100644 index 000000000..d171a76be --- /dev/null +++ b/src/kandroidextras/fake/qt6/QJniEnvironment @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MOCK_QJNIENVIRONMENT_H +#define KANDROIDEXTRAS_MOCK_QJNIENVIRONMENT_H + +#include "mock_jnienvironment.h" + +class QJniEnvironment : public KAndroidExtras::MockJniEnvironment +{ +public: + inline JNIEnv* jniEnv() const { return &m_env; } +}; + +#endif diff --git a/src/kandroidextras/fake/qt6/QJniObject b/src/kandroidextras/fake/qt6/QJniObject new file mode 100644 index 000000000..614dca598 --- /dev/null +++ b/src/kandroidextras/fake/qt6/QJniObject @@ -0,0 +1,6 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "qjniobject.h" diff --git a/src/kandroidextras/fake/qt6/private/qandroidextras_p.h b/src/kandroidextras/fake/qt6/private/qandroidextras_p.h new file mode 100644 index 000000000..43f02f499 --- /dev/null +++ b/src/kandroidextras/fake/qt6/private/qandroidextras_p.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MAKE_QTANDROIDEXTRAS_P_H +#define KANDROIDEXTRAS_MAKE_QTANDROIDEXTRAS_P_H + +#include + +/** Mock object for QtAndroid namespace. */ +namespace QtAndroidPrivate +{ +inline void startActivity(const QJniObject &, int) +{ +} +} + +#endif diff --git a/src/kandroidextras/fake/qt6/qjniobject.h b/src/kandroidextras/fake/qt6/qjniobject.h new file mode 100644 index 000000000..9ae171d1a --- /dev/null +++ b/src/kandroidextras/fake/qt6/qjniobject.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_MOCK_QJNIOBJECT_H +#define KANDROIDEXTRAS_MOCK_QJNIOBJECT_H + +#include "mock_jniobject.h" + +/** Mock object for QJniObject outside of Android, for automated testing. */ +class KANDROIDEXTRAS_EXPORT QJniObject : public KAndroidExtras::Internal::MockJniObject +{ +public: + using MockJniObject::MockJniObject; +}; + +Q_DECLARE_METATYPE(QJniObject) + +#endif diff --git a/src/kandroidextras/java/java_headers.cpp b/src/kandroidextras/java/java_headers.cpp new file mode 100644 index 000000000..f84ae1540 --- /dev/null +++ b/src/kandroidextras/java/java_headers.cpp @@ -0,0 +1,9 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +// list all headers here that have no .cpp file +// this only serves as a guarantee that these headers actually compile +#include "javatypes.h" diff --git a/src/kandroidextras/java/javalocale.cpp b/src/kandroidextras/java/javalocale.cpp new file mode 100644 index 000000000..484bb2f6c --- /dev/null +++ b/src/kandroidextras/java/javalocale.cpp @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "javalocale.h" + +#include +#include + +#include + +using namespace KAndroidExtras; + +QJniObject Locale::fromLocale(const QLocale &locale) +{ + auto lang = QJniObject::fromString(QLocale::languageToString(locale.language())); + auto country = QJniObject::fromString(QLocale::countryToString(locale.country())); + auto script = QJniObject::fromString(QLocale::scriptToString(locale.script())); + + return QJniObject(Jni::typeName(), + (const char *)Jni::signature(), + lang.object(), + country.object(), + script.object()); +} + +QJniObject Locale::current() +{ + return fromLocale(QLocale()); +} diff --git a/src/kandroidextras/java/javalocale.h b/src/kandroidextras/java/javalocale.h new file mode 100644 index 000000000..ad8427635 --- /dev/null +++ b/src/kandroidextras/java/javalocale.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_LOCALE_H +#define KANDROIDEXTRAS_LOCALE_H + +#include "kandroidextras_export.h" + +class QJniObject; +class QLocale; + +namespace KAndroidExtras +{ + +/** Conversion methods between java.util.Locale and QLocale. + * @note Do not rename this file to locale.h, that clashes with POSIX locale.h when your + * include paths are unfortunately set up causing bizarre compilation issues. + */ +namespace Locale +{ +/** Create an java.util.Locale object from a QLocale. */ +KANDROIDEXTRAS_EXPORT QJniObject fromLocale(const QLocale &locale); + +/** Create an java.util.Locale object for the current QLocale. */ +KANDROIDEXTRAS_EXPORT QJniObject current(); +} + +} + +#endif // KANDROIDEXTRAS_LOCALE_H diff --git a/src/kandroidextras/java/javatypes.h b/src/kandroidextras/java/javatypes.h new file mode 100644 index 000000000..fa216dfe1 --- /dev/null +++ b/src/kandroidextras/java/javatypes.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JAVATYPES_H +#define KANDROIDEXTRAS_JAVATYPES_H + +#include +#include + +namespace KAndroidExtras +{ + +JNI_TYPE(java, io, File) +JNI_TYPE(java, lang, String) +JNI_TYPE(java, util, Locale) + +JNI_DECLARE_CONVERTER(java::lang::String, QString, (value.toString()), (QJniObject::fromString(value))) + +} + +#endif // KANDROIDEXTRAS_JAVATYPES_H diff --git a/src/kandroidextras/jni/jni_headers.cpp b/src/kandroidextras/jni/jni_headers.cpp new file mode 100644 index 000000000..5a1ece371 --- /dev/null +++ b/src/kandroidextras/jni/jni_headers.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +// list all headers here that have no .cpp file +// this only serves as a guarantee that these headers actually compile +#include "jniargument.h" +#include "jniarray.h" +#include "jnimethod.h" +#include "jniobject.h" +#include "jnipp.h" +#include "jniprimitivetypes.h" +#include "jniproperty.h" +#include "jnireturnvalue.h" +#include "jnisignature.h" +#include "jnitypes.h" +#include "jnitypetraits.h" diff --git a/src/kandroidextras/jni/jniargument.h b/src/kandroidextras/jni/jniargument.h new file mode 100644 index 000000000..9d99d1300 --- /dev/null +++ b/src/kandroidextras/jni/jniargument.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2021-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIARGUMENTVALUE_H +#define KANDROIDEXTRAS_JNIARGUMENTVALUE_H + +#include "jniobject.h" +#include "jnitypetraits.h" + +namespace KAndroidExtras +{ +namespace Jni +{ +template +class Array; +} + +///@cond internal +namespace Internal +{ +/** Call argument wrapper. */ +template> +struct argument { + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + typedef std::conditional_t::value, T, const Jni::Object &> type; + static inline constexpr auto toCallArgument(type value) + { + if constexpr (Jni::is_primitive_type::value) { + return primitive_value::toJni(value); + } else { + return value.jniHandle().object(); + } + } +}; +template +struct argument> { + typedef const T &type; + static inline auto toCallArgument(const T &value) + { + return Jni::handle(value).object(); + } +}; +template +struct argument> { + typedef const Jni::Array &type; + static inline auto toCallArgument(const Jni::Array &value) + { + return value.jniHandle().object(); + } +}; +} +///@endcond +} + +#endif diff --git a/src/kandroidextras/jni/jniarray.h b/src/kandroidextras/jni/jniarray.h new file mode 100644 index 000000000..4a6afe06b --- /dev/null +++ b/src/kandroidextras/jni/jniarray.h @@ -0,0 +1,395 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIARRAY_H +#define KANDROIDEXTRAS_JNIARRAY_H + +#include "jniargument.h" +#include "jnireturnvalue.h" +#include "jnitypetraits.h" + +#include + +namespace KAndroidExtras +{ + +///@cond internal +namespace Internal +{ + +/** Primitive type array type traits. */ +template +struct array_trait { + typedef jobjectArray type; +}; + +#define MAKE_ARRAY_TRAIT(base_type, type_name) \ + template<> \ + struct array_trait { \ + typedef base_type##Array type; \ + static inline type newArray(QJniEnvironment &env, jsize size) \ + { \ + return env->New##type_name##Array(size); \ + } \ + static inline base_type *getArrayElements(QJniEnvironment &env, type array, jboolean *isCopy) \ + { \ + return env->Get##type_name##ArrayElements(array, isCopy); \ + } \ + static inline void releaseArrayElements(QJniEnvironment &env, type array, base_type *data, jint mode) \ + { \ + return env->Release##type_name##ArrayElements(array, data, mode); \ + } \ + static inline void setArrayRegion(QJniEnvironment &env, type array, jsize start, jsize length, const base_type *data) \ + { \ + env->Set##type_name##ArrayRegion(array, start, length, data); \ + } \ + }; + +MAKE_ARRAY_TRAIT(jboolean, Boolean) +MAKE_ARRAY_TRAIT(jbyte, Byte) +MAKE_ARRAY_TRAIT(jchar, Char) +MAKE_ARRAY_TRAIT(jshort, Short) +MAKE_ARRAY_TRAIT(jint, Int) +MAKE_ARRAY_TRAIT(jlong, Long) +MAKE_ARRAY_TRAIT(jfloat, Float) +MAKE_ARRAY_TRAIT(jdouble, Double) + +#undef MAKE_ARRAY_TRAIT + +/** Meta function for retrieving a JNI array .*/ +template +struct FromArray { +}; + +template +struct FromArray { + inline auto operator()(const QJniObject &array) const + { + if (!array.isValid()) { + return Container{}; + } + const auto a = static_cast(array.object()); + QJniEnvironment env; + const auto size = env->GetArrayLength(a); + Container r; + r.reserve(size); + for (auto i = 0; i < size; ++i) { + r.push_back(QJniObject::fromLocalRef(env->GetObjectArrayElement(a, i))); + } + return r; + } +}; + +template +struct FromArray { + inline auto operator()(const QJniObject &array) const + { + if (!array.isValid()) { + return Container{}; + } + const auto a = static_cast(array.object()); + QJniEnvironment env; + const auto size = env->GetArrayLength(a); + Container r; + r.reserve(size); + for (auto i = 0; i < size; ++i) { + r.push_back(Jni::reverse_converter::type::convert(QJniObject::fromLocalRef(env->GetObjectArrayElement(a, i)))); + } + return r; + } +}; + +// specializations for primitive types +template +struct FromArray { + typedef array_trait _t; + inline auto operator()(const QJniObject &array) const + { + if (!array.isValid()) { + return Container{}; + } + + const auto a = static_cast(array.object()); + QJniEnvironment env; + const auto size = env->GetArrayLength(a); + Container r; + r.reserve(size); + + auto data = _t::getArrayElements(env, a, nullptr); + std::copy(data, data + size, std::back_inserter(r)); + _t::releaseArrayElements(env, a, data, JNI_ABORT); + + return r; + } +}; + +// array wrapper, common base for primitive and non-primitive types +template +class ArrayImplBase +{ +public: + typedef T value_type; + typedef jsize size_type; + typedef jsize difference_type; + + ArrayImplBase() = default; + inline ArrayImplBase(const QJniObject &array) + : m_array(array) + { + } + ArrayImplBase(const ArrayImplBase &) = default; + ArrayImplBase(ArrayImplBase &&) = default; + + inline size_type size() const + { + if (!m_array.isValid()) { + return 0; + } + const auto a = static_cast(m_array.object()); + QJniEnvironment env; + return env->GetArrayLength(a); + } + + inline operator QJniObject() const + { + return m_array; + } + inline QJniObject jniHandle() const + { + return m_array; + } + +protected: + typedef array_trait _t; + + typename _t::type handle() const + { + return static_cast(m_array.object()); + } + + QJniObject m_array; +}; + +template +class ArrayImpl +{ +}; + +// array wrapper for primitive types +template +class ArrayImpl : public ArrayImplBase +{ + static_assert(!Internal::is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + +public: + inline ArrayImpl(const QJniObject &array) + : ArrayImplBase(array) + { + // ### do this on demand? + getArrayElements(); + } + + /** Create a new array with @p size elements. */ + inline explicit ArrayImpl(jsize size) + { + QJniEnvironment env; + ArrayImplBase::m_array = QJniObject::fromLocalRef(ArrayImplBase::_t::newArray(env, size)); + getArrayElements(); + } + + ArrayImpl() = default; + ArrayImpl(const ArrayImpl &) = delete; // ### ref count m_data and allow copying? + ArrayImpl(ArrayImpl &&) = default; + ~ArrayImpl() + { + QJniEnvironment env; + ArrayImplBase::_t::releaseArrayElements(env, this->handle(), m_data, JNI_ABORT); + } + + T operator[](jsize index) const + { + return m_data[index]; + } + + T *begin() const + { + return m_data; + } + T *end() const + { + return m_data + ArrayImplBase::size(); + } + +private: + inline void getArrayElements() + { + if (!ArrayImplBase::m_array.isValid()) { + return; + } + QJniEnvironment env; + m_data = ArrayImplBase::_t::getArrayElements(env, this->handle(), nullptr); + } + + T *m_data = nullptr; +}; + +// array wrapper for non-primitive types +template +class ArrayImpl : public ArrayImplBase +{ +public: + using ArrayImplBase::ArrayImplBase; + + /** Create a new array with @p size elements initialized with @p value. */ + explicit inline ArrayImpl(jsize size, typename Internal::argument::type value) + { + QJniEnvironment env; + auto clazz = env.findClass(Jni::typeName()); + ArrayImplBase::m_array = QJniObject::fromLocalRef(env->NewObjectArray(size, clazz, Internal::argument::toCallArgument(value))); + } + + /** Create a new array with @p size null elements. */ + explicit inline ArrayImpl(jsize size, std::nullptr_t = nullptr) + { + QJniEnvironment env; + auto clazz = env.findClass(Jni::typeName()); + ArrayImplBase::m_array = QJniObject::fromLocalRef(env->NewObjectArray(size, clazz, nullptr)); + } + + ArrayImpl() = default; + ArrayImpl(const ArrayImpl &) = default; + ArrayImpl(ArrayImpl &&) = default; + + auto operator[](jsize index) const + { + QJniEnvironment env; + return Internal::return_wrapper::toReturnValue(QJniObject::fromLocalRef(env->GetObjectArrayElement(this->handle(), index))); + } + + class ref + { + public: + inline operator auto() + { + QJniEnvironment env; + return Internal::return_wrapper::toReturnValue(QJniObject::fromLocalRef(env->GetObjectArrayElement(c.handle(), index))); + } + inline ref &operator=(typename Internal::argument::type v) + { + QJniEnvironment env; + env->SetObjectArrayElement(c.handle(), index, Internal::argument::toCallArgument(v)); + return *this; + } + + private: + ArrayImpl &c; + jsize index; + + friend class ArrayImpl; + inline ref(jsize _i, ArrayImpl &_c) + : c(_c) + , index(_i) + { + } + }; + ref operator[](jsize index) + { + return ref(index, *this); + } + + class const_iterator + { + const ArrayImpl &c; + jsize i = 0; + + public: + typedef jsize difference_type; + typedef T value_type; + typedef T &reference; + typedef std::random_access_iterator_tag iterator_category; + typedef T *pointer; + + const_iterator(const ArrayImpl &_c, jsize _i) + : c(_c) + , i(_i) + { + } + + difference_type operator-(const_iterator other) const + { + return i - other.i; + } + + const_iterator &operator++() + { + ++i; + return *this; + } + const_iterator operator++(int) + { + return const_iterator(c, i++); + } + + bool operator==(const_iterator other) const + { + return i == other.i; + } + bool operator!=(const_iterator other) const + { + return i != other.i; + } + + auto operator*() const + { + return c[i]; + } + }; + + const_iterator begin() const + { + return const_iterator(*this, 0); + } + const_iterator end() const + { + return const_iterator(*this, ArrayImplBase::size()); + } +}; + +} +///@endcond + +namespace Jni +{ + +/** Convert a JNI array to a C++ container. + * Container value types can be any of + * - QJniObject + * - a primitive JNI type + * - a type with a conversion defined with @c JNI_DECLARE_CONVERTER + */ +template +constexpr + __attribute__((__unused__)) Internal::FromArray::value> + fromArray = {}; + +/** Container-like wrapper for JNI arrays. */ +template +class Array : public Internal::ArrayImpl::value> +{ +public: + using Internal::ArrayImpl::value>::ArrayImpl; + template + inline operator Container() const + { + // ### should this be re-implemented in terms of Jni::Array API rather than direct JNI access? + return Jni::fromArray(this->m_array); + } +}; +} + +} + +#endif diff --git a/src/kandroidextras/jni/jnimethod.h b/src/kandroidextras/jni/jnimethod.h new file mode 100644 index 000000000..04c6b2b23 --- /dev/null +++ b/src/kandroidextras/jni/jnimethod.h @@ -0,0 +1,192 @@ +/* + SPDX-FileCopyrightText: 2021 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIMETHOD_H +#define KANDROIDEXTRAS_JNIMETHOD_H + +#include "jniargument.h" +#include "jniarray.h" +#include "jniobject.h" +#include "jnipp.h" +#include "jnireturnvalue.h" +#include "jnitypetraits.h" + +namespace KAndroidExtras +{ +///@cond internal + +// method parameter generation +#define JNI_PARAM(Type, Name) typename KAndroidExtras::Internal::argument::type Name + +#define JNI_PARAMS_0(accu, arg) +#define JNI_PARAMS_1(accu, arg, ...) JNI_PARAM(arg, a1) +#define JNI_PARAMS_2(accu, arg, ...) JNI_PARAM(arg, a2), JNI_PARAMS_1(accu, __VA_ARGS__) +#define JNI_PARAMS_3(accu, arg, ...) JNI_PARAM(arg, a3), JNI_PARAMS_2(accu, __VA_ARGS__) + +#define JNI_PARAMS_(N, accu, ...) JNI_PP_CONCAT(JNI_PARAMS_, N)(accu, __VA_ARGS__) +#define JNI_PARAMS(...) JNI_PARAMS_(JNI_PP_NARG(__VA_ARGS__), "", __VA_ARGS__) + +// method argument forwarding generation +#define JNI_ARG(Type, Name) KAndroidExtras::Internal::argument::toCallArgument(Name) +#define JNI_ARGS_0(accu, arg) +#define JNI_ARGS_1(accu, arg, ...) JNI_ARG(arg, a1) +#define JNI_ARGS_2(accu, arg, ...) JNI_ARG(arg, a2), JNI_ARGS_1(accu, __VA_ARGS__) +#define JNI_ARGS_3(accu, arg, ...) JNI_ARG(arg, a3), JNI_ARGS_2(accu, __VA_ARGS__) + +#define JNI_ARGS_(N, accu, ...) JNI_PP_CONCAT(JNI_ARGS_, N)(accu, __VA_ARGS__) +#define JNI_ARGS(...) JNI_ARGS_(JNI_PP_NARG(__VA_ARGS__), "", __VA_ARGS__) + +namespace Internal +{ +// method call wrapper +template +struct caller { + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + template + static auto call(const QJniObject &handle, const char *name, const char *signature, Args &&...args) + { + if constexpr (std::is_same_v) { + handle.callMethod(name, signature, std::forward(args)...); + } else if constexpr (Jni::is_primitive_type::value) { + return Internal::return_wrapper::toReturnValue( + handle.callMethod::JniType>(name, signature, std::forward(args)...)); + } else { + return Internal::return_wrapper::toReturnValue(handle.callObjectMethod(name, signature, std::forward(args)...)); + } + } + + static auto call(const QJniObject &handle, const char *name, const char *signature) + { + if constexpr (std::is_same_v) { + return handle.callMethod(name, signature); + } else if constexpr (Jni::is_primitive_type::value) { + return Internal::return_wrapper::toReturnValue(handle.callMethod::JniType>(name, signature)); + } else { + return Internal::return_wrapper::toReturnValue(handle.callObjectMethod(name, signature)); + } + } +}; + +// static method call wrapper +template +struct static_caller { + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + template + static auto call(const char *className, const char *name, const char *signature, Args &&...args) + { + if constexpr (std::is_same_v) { + return QJniObject::callStaticMethod(className, name, signature, std::forward(args)...); + } else if constexpr (Jni::is_primitive_type::value) { + return Internal::return_wrapper::toReturnValue( + QJniObject::callStaticMethod::JniType>(className, name, signature, std::forward(args)...)); + } else { + return Internal::return_wrapper::toReturnValue(QJniObject::callStaticObjectMethod(className, name, signature, std::forward(args)...)); + } + } + static auto call(const char *className, const char *name, const char *signature) + { + if constexpr (std::is_same_v) { + return QJniObject::callStaticMethod(className, name, signature); + } else if constexpr (Jni::is_primitive_type::value) { + return Internal::return_wrapper::toReturnValue( + QJniObject::callStaticMethod::JniType>(className, name, signature)); + } else { + return Internal::return_wrapper::toReturnValue(QJniObject::callStaticObjectMethod(className, name, signature)); + } + } +}; +} +///@endcond + +/** + * Wrap a JNI method call. + * This will add a method named @p Name to the current class. Argument types are checked at compile time, + * with the following inputs being accepted: + * - primitive types have to match exactly + * - non-primitive types can be either passed as @c QJniObject instance or with a type that has an + * conversion registered with @c JNI_DECLARE_CONVERTER. + * + * The return type of the method is determined as follows: + * - primitive types are returned directly + * - non-primitive types without a registered type conversion are returned as @c QJniObject. + * - non-primitive types with a registered type conversion are returned in a wrapper class that can + * be implicitly converted either to the destination type of the conversion, or a @c QJniObject. + * This allows to avoid type conversion when chaining calls for example, it however needs additional + * care when used in combination with automatic type deduction. + * - array return types also result in a wrapper class that can be implicitly converted to a sequential + * container or a @p QJniObject representing the JNI array. + * + * Thie macro can only be placed in classes having the @c JNI_OBJECT macro. + * + * @param RetT The return type. Must either be a primitive type or a type declared with @c JNI_TYPE + * @param Name The name of the method. Must match the JNI method to be called exactly. + * @param Args A list or argument types (can be empty). Must either be primitive types or types declared + * with @c JNI_TYPE. + */ +#define JNI_METHOD(RetT, Name, ...) \ + inline auto Name(JNI_PARAMS(__VA_ARGS__)) const \ + { \ + using namespace KAndroidExtras; \ + return Internal::caller::call(jniHandle(), "" #Name, Jni::signature() __VA_OPT__(, ) JNI_ARGS(__VA_ARGS__)); \ + } + +/** + * Wrap a JNI static method call. + * This will add a static method named @p Name to the current class. Argument types are checked at compile time, + * with the following inputs being accepted: + * - primitive types have to match exactly + * - non-primitive types can be either passed as @c QJniObject instance or with a type that has an + * conversion registered with @c JNI_DECLARE_CONVERTER. + * + * The return type of the method is determined as follows: + * - primitive types are returned directly + * - non-primitive types without a registered type conversion are returned as @c QJniObject. + * - non-primitive types with a registered type conversion are returned in a wrapper class that can + * be implicitly converted either to the destination type of the conversion, or a @c QJniObject. + * This allows to avoid type conversion when chaining calls for example, it however needs additional + * care when used in combination with automatic type deduction. + * - array return types also result in a wrapper class that can be implicitly converted to a sequential + * container or a @p QJniObject representing the JNI array. + * + * Thie macro can only be placed in classes having the @c JNI_UNMANAGED_OBJECT or @c JNI_OBJECT macro. + * + * @param RetT The return type. Must either be a primitive type or a type declared with @c JNI_TYPE + * @param Name The name of the method. Must match the JNI method to be called exactly. + * @param Args A list or argument types (can be empty). Must either be primitive types or types declared + * with @c JNI_TYPE. + */ +#define JNI_STATIC_METHOD(RetT, Name, ...) \ + static inline auto Name(JNI_PARAMS(__VA_ARGS__)) \ + { \ + using namespace KAndroidExtras; \ + return Internal::static_caller::call(Jni::typeName<_jni_ThisType>(), \ + "" #Name, \ + Jni::signature() __VA_OPT__(, ) JNI_ARGS(__VA_ARGS__)); \ + } + +/** + * Wrap a JNI constructor call. + * This will add a constructor named @p Name to the current class. Argument types are checked at compile time, + * with the following inputs being accepted: + * - primitive types have to match exactly + * - non-primitive types can be either passed as @c QJniObject instance or with a type that has an + * conversion registered with @c JNI_DECLARE_CONVERTER. + * + * Thie macro can only be placed in classes having @c JNI_OBJECT macro. + * + * @param Name The name of the method. Must match the JNI method to be called exactly. + * @param Args A list or argument types (can be empty). Must either be primitive types or types declared + * with @c JNI_TYPE. + */ +#define JNI_CONSTRUCTOR(Name, ...) \ + inline Name(JNI_PARAMS(__VA_ARGS__)) \ + { \ + using namespace KAndroidExtras; \ + setJniHandle(QJniObject(Jni::typeName<_jni_ThisType>(), (const char *)Jni::signature() __VA_OPT__(, ) JNI_ARGS(__VA_ARGS__))); \ + } + +} + +#endif diff --git a/src/kandroidextras/jni/jniobject.h b/src/kandroidextras/jni/jniobject.h new file mode 100644 index 000000000..606cdd090 --- /dev/null +++ b/src/kandroidextras/jni/jniobject.h @@ -0,0 +1,188 @@ +/* + SPDX-FileCopyrightText: 2019-2021 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIOBJECT_H +#define KANDROIDEXTRAS_JNIOBJECT_H + +#include "jnitypetraits.h" + +namespace KAndroidExtras +{ + +namespace java +{ +namespace lang +{ +struct String; +} +} + +namespace Internal +{ +struct FromHandleTag { +}; +} + +namespace Jni +{ + +template +class Object; + +template +inline QJniObject handle(const T &wrapper) +{ + return wrapper.jniHandle(); +} + +/** Convert an untyped JNI object handle to a typed wrapper. + * This is essentially the JNI equivalent to a reinterpret_cast from a void*. + * Only use this if you are sure @p handle has the correct type, otherwise + * things will horribly go wrong. + */ +template +inline auto fromHandle(const QJniObject &handle) +{ + if constexpr (Jni::is_object_wrapper::value) { + return T(handle, Internal::FromHandleTag()); + } else { + return Jni::Object(handle); + } +} + +template +inline auto fromHandle(jobject handle) +{ + return fromHandle(QJniObject(handle)); +} + +/** Wrapper for JNI objects with a convertible C++ type. + * This provides implicit on-demand conversion to the C++ type, for types + * that don't have a manually defined wrapper. + */ +template +class Object +{ +private: + template + class _dummy_t + { + }; + +public: + inline explicit Object(const QJniObject &v) + : m_handle(v) + { + } + + // implicit conversion from a compatible C++ type + inline Object(const std::conditional_t::type, void>, _dummy_t, typename Jni::converter::type> &v) + : m_handle(Jni::reverse_converter::type::convert(v)) + { + } + + // implicit conversion from a custom wrapper for the same type + template, WrapperT>> + inline Object(const WrapperT &w) + : m_handle(Jni::handle(w)) + { + } + + // special-case QString, as that often comes out of implicitly converted types, + // and thus can't be consumed by another implicit conversion step + template, StrT>> + inline Object(const QString &s) + : m_handle(QJniObject::fromString(s)) + { + } + + // special case for null values + inline Object(std::nullptr_t) + : m_handle((jobject) nullptr) + { + } + + inline operator QJniObject() const + { + return m_handle; + } + inline operator typename Jni::converter::type() const + { + return Jni::converter::convert(m_handle); + } + + inline QJniObject jniHandle() const + { + return m_handle; + } + + // forward basic QJniObject API + inline bool isValid() const + { + return m_handle.isValid(); + } + template, StrT>> + inline QString toString() const + { + return m_handle.toString(); + } + +private: + QJniObject m_handle; +}; + +/** Annotates a class for holding JNI method or property wrappers. + * + * Use this if the class only has static methods or constants. For methods + * and properties, you either need to use @c JNI_OBJECT instead, or make + * sure there is a @p jniHandle() method returning a @c QJniObject + * representing the current instance. + * + * @param Class the name of the class this is added to. + * @param BaseType the Java type this class represents, defined by the + * @c JNI_TYPE or @c JNI_NESTED_TYPE macros. + */ +#define JNI_UNMANAGED_OBJECT(Class, BaseType) \ +public: \ + typedef Class _jni_ThisType; \ + typedef BaseType _jni_BaseType; \ + \ +private: \ + static inline constexpr const char *jniName() \ + { \ + return KAndroidExtras::Jni::typeName(); \ + } \ + friend constexpr const char *KAndroidExtras::Jni::typeName(); + +/** Annotates a class for holding JNI method or property wrappers. + * + * @param Class the name of the class this is added to. + * @param BaseType the Java type this class represents, defined by the + * @c JNI_TYPE or @c JNI_NESTED_TYPE macros. + */ +#define JNI_OBJECT(Class, BaseType) \ + JNI_UNMANAGED_OBJECT(Class, BaseType) \ +private: \ + QJniObject _m_jni_handle; \ + inline QJniObject jniHandle() const \ + { \ + return _m_jni_handle; \ + } \ + inline void setJniHandle(const QJniObject &h) \ + { \ + _m_jni_handle = h; \ + } \ + friend QJniObject KAndroidExtras::Jni::handle(const Class &); \ + friend auto KAndroidExtras::Jni::fromHandle(const QJniObject &); \ + explicit inline Class(const QJniObject &handle, KAndroidExtras::Internal::FromHandleTag) \ + : _m_jni_handle(handle) \ + { \ + } + +} +} + +#endif diff --git a/src/kandroidextras/jni/jnipp.h b/src/kandroidextras/jni/jnipp.h new file mode 100644 index 000000000..91776124d --- /dev/null +++ b/src/kandroidextras/jni/jnipp.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2019-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIPP_H +#define KANDROIDEXTRAS_JNIPP_H + +/** @file jnipp.h + * Preprocessor macro implementation details. + */ + +///@cond internal + +// determine how many elements are in __VA_ARGS__ +#define JNI_PP_NARG(...) JNI_PP_NARG_(__VA_ARGS__ __VA_OPT__(, ) JNI_PP_RSEQ_N()) +#define JNI_PP_NARG_(...) JNI_PP_ARG_N(__VA_ARGS__) +#define JNI_PP_ARG_N(_1, _2, _3, _4, _5, _6, _7, N, ...) N +#define JNI_PP_RSEQ_N() 7, 6, 5, 4, 3, 2, 1, 0 + +// preprocessor-level token concat +#define JNI_PP_CONCAT(arg1, arg2) JNI_PP_CONCAT1(arg1, arg2) +#define JNI_PP_CONCAT1(arg1, arg2) JNI_PP_CONCAT2(arg1, arg2) +#define JNI_PP_CONCAT2(arg1, arg2) arg1##arg2 + +///@endcond + +#endif // KANDROIDEXTRAS_JNIPP_H diff --git a/src/kandroidextras/jni/jniprimitivetypes.h b/src/kandroidextras/jni/jniprimitivetypes.h new file mode 100644 index 000000000..5d49c48e5 --- /dev/null +++ b/src/kandroidextras/jni/jniprimitivetypes.h @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2020-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIPRIMITIVETYPES_H +#define KANDROIDEXTRAS_JNIPRIMITIVETYPES_H + +#include "jni.h" + +#include + +namespace KAndroidExtras +{ + +namespace Jni +{ + +/** Type trait to check whether @tparam T is a primitive JNI type or an object type. */ +template +struct is_primitive_type : std::false_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +template<> +struct is_primitive_type : std::true_type { +}; +// special case for bool <-> jboolean conversion +template<> +struct is_primitive_type : std::true_type { +}; + +} + +namespace Internal +{ + +/** Utility trait to check for basic C++ types that are not JNI primitive types. */ +template +struct is_invalid_primitive_type : std::false_type { +}; +template<> +struct is_invalid_primitive_type : std::true_type { +}; +template<> +struct is_invalid_primitive_type : std::true_type { +}; +template<> +struct is_invalid_primitive_type : std::true_type { +}; +template<> +struct is_invalid_primitive_type : std::true_type { +}; + +// C++ primitive to JNI primitive conversion +template +struct primitive_value { + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + typedef T JniType; + typedef T NativeType; + static constexpr inline T toJni(T value) + { + return value; + } + static constexpr inline T fromJni(T value) + { + return value; + } +}; +template<> +struct primitive_value { +}; +template<> +struct primitive_value { + typedef jboolean JniType; + typedef bool NativeType; + static constexpr inline jboolean toJni(bool value) + { + return value ? 1 : 0; + } + static constexpr inline bool fromJni(jboolean value) + { + return value != 0; + } +}; +template<> +struct primitive_value { + typedef jboolean JniType; + typedef bool NativeType; + static constexpr inline jboolean toJni(bool value) + { + return value ? 1 : 0; + } + static constexpr inline bool fromJni(jboolean value) + { + return value != 0; + } +}; + +} +} + +#endif diff --git a/src/kandroidextras/jni/jniproperty.h b/src/kandroidextras/jni/jniproperty.h new file mode 100644 index 000000000..fb29a2e0b --- /dev/null +++ b/src/kandroidextras/jni/jniproperty.h @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIPROPERTIES_H +#define KANDROIDEXTRAS_JNIPROPERTIES_H + +#include "jniargument.h" +#include "jniobject.h" +#include "jnisignature.h" +#include "jnitypes.h" +#include "jnitypetraits.h" + +#include + +namespace KAndroidExtras +{ + +namespace Jni +{ +template +class Array; +} + +/** @cond internal */ +namespace Internal +{ + +/** Wrapper for static properties. */ +template +struct StaticProperty { +}; +template +struct StaticProperty { + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + inline QJniObject get() const + { + return QJniObject::getStaticObjectField(Jni::typeName(), Jni::typeName(), Jni::signature()); + } + inline operator QJniObject() const + { + return get(); + } + inline operator typename Jni::converter::type() const + { + return Jni::converter::convert(get()); + } + template::value, RetT>> + inline operator Jni::Object() const + { + return Jni::Object(get()); + } +}; + +template +struct StaticProperty { + inline operator auto() const + { + return primitive_value::fromJni( + QJniObject::getStaticField::JniType>(Jni::typeName(), Jni::typeName())); + } +}; + +/** Shared code for non-static property wrappers. */ +template +class PropertyBase +{ +protected: + inline QJniObject handle() const + { + const auto owner = reinterpret_cast(reinterpret_cast(this) - OffsetHolder::offset()); + return owner->jniHandle(); + } +}; + +/** Wrapper for non-static properties. */ +template +struct Property { +}; +template +class Property : public PropertyBase +{ +private: + struct _jni_NoType { + }; + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + +public: + inline QJniObject get() const + { + return this->handle().getObjectField(Jni::typeName(), Jni::signature()); + } + inline operator QJniObject() const + { + return get(); + } + inline operator typename Jni::converter::type() const + { + return Jni::converter::convert(get()); + } + template::value, RetT>> + inline operator PropType() const + { + return PropType(get()); + } + template::value, RetT>> + inline operator Jni::Object() const + { + return Jni::Object(get()); + } + + inline Property &operator=(typename Internal::argument::type value) + { + this->handle().setField(Jni::typeName(), Jni::signature(), Internal::argument::toCallArgument(value)); + return *this; + } + inline Property &operator=(const QJniObject &value) + { + this->handle().setField(Jni::typeName(), Jni::signature(), value.object()); + return *this; + } + inline Property &operator=(const typename std::conditional::type, void>, + _jni_NoType, + typename Jni::converter::type>::type &value) + { + this->handle().setField(Jni::typeName(), Jni::signature(), Jni::reverse_converter::type::convert(value).object()); + return *this; + } + + // special case for string comparison, which is often done against different types and thus the implicit conversion operator + // isn't going to be enough + template, CmpT>> + inline bool operator==(const CmpT &other) const + { + return QString(*this) == other; + } +}; + +template +class Property : public PropertyBase +{ +public: + inline operator auto() const + { + return primitive_value::fromJni(this->handle().template getField::JniType>(Jni::typeName())); + } + inline Property &operator=(PropType value) + { + this->handle().setField(Jni::typeName(), primitive_value::toJni(value)); + return *this; + } +}; + +// TODO KF6: can be replaced by QT_WARNING_DISABLE_INVALID_OFFSETOF +#if defined(Q_CC_CLANG) +#define JNI_WARNING_DISABLE_INVALID_OFFSETOF QT_WARNING_DISABLE_CLANG("-Winvalid-offsetof") +#elif defined(Q_CC_GNU) +#define JNI_WARNING_DISABLE_INVALID_OFFSETOF QT_WARNING_DISABLE_GCC("-Winvalid-offsetof") +#else +#define JNI_WARNING_DISABLE_INVALID_OFFSETOF +#endif + +/** @endcond */ +} + +/** + * Wrap a static final property. + * This will add a public static member named @p name to the current class. This member defines an + * implicit conversion operator which will trigger the corresponding a JNI read operation. + * Can only be placed in classes with a @c JNI_OBJECT. + * + * @note Make sure to access this member with a specific type, assigning to an @c auto variable will + * copy the wrapper type, not read the property value. + * + * @param type The data type of the property. + * @param name The name of the property. + */ +#define JNI_CONSTANT(type, name) \ +private: \ + struct _jni_##name##__NameHolder { \ + static constexpr const char *jniName() \ + { \ + return "" #name; \ + } \ + }; \ + \ +public: \ + static inline const KAndroidExtras::Internal::StaticProperty::value> name; + +/** + * Wrap a member property. + * This will add a public zero-size member named @p name to the current class. This member defines an + * implicit conversion operator which will trigger the corresponding a JNI read operation, as well + * as an overloaded assignment operator for the corresponding write operation. + * Can only be placed in classes with a @c JNI_OBJECT. + * + * @note Make sure to access this member with a specific type, assigning to an @c auto variable will + * copy the wrapper type, not read the property value. + * + * @param type The data type of the property. + * @param name The name of the property. + */ +#define JNI_PROPERTY(type, name) \ +private: \ + struct _jni_##name##__NameHolder { \ + static constexpr const char *jniName() \ + { \ + return "" #name; \ + } \ + }; \ + struct _jni_##name##__OffsetHolder { \ + static constexpr std::size_t offset() \ + { \ + QT_WARNING_PUSH JNI_WARNING_DISABLE_INVALID_OFFSETOF return offsetof(_jni_ThisType, name); \ + QT_WARNING_POP \ + } \ + }; \ + friend class KAndroidExtras::Internal::PropertyBase<_jni_ThisType, _jni_##name##__OffsetHolder>; \ + \ +public: \ + [[no_unique_address]] KAndroidExtras::Internal:: \ + Property::value> name; +} + +#endif // KANDROIDEXTRAS_JNIPROPERTIES_H diff --git a/src/kandroidextras/jni/jnireturnvalue.h b/src/kandroidextras/jni/jnireturnvalue.h new file mode 100644 index 000000000..0d6e43ceb --- /dev/null +++ b/src/kandroidextras/jni/jnireturnvalue.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2021-2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNIRETURNVALUE_H +#define KANDROIDEXTRAS_JNIRETURNVALUE_H + +#include "jniobject.h" +#include "jnitypetraits.h" + +namespace KAndroidExtras +{ +namespace Jni +{ +template +class Array; +} + +///@cond internal +namespace Internal +{ +/** Return value wrapping helper. */ +template> +class return_wrapper +{ + static_assert(!is_invalid_primitive_type::value, "Using an incompatible primitive type!"); + static inline constexpr bool is_primitive = Jni::is_primitive_type::value; + static inline constexpr bool is_convertible = !std::is_same_v::type, void>; + + typedef std::conditional_t JniReturnT; + +public: + static inline constexpr auto toReturnValue(JniReturnT value) + { + if constexpr (is_convertible) { + return Jni::Object(value); + } else if constexpr (is_primitive) { + return primitive_value::fromJni(value); + } else { + return value; + } + } +}; +template +class return_wrapper> +{ +public: + static inline auto toReturnValue(const QJniObject &value) + { + return Jni::fromHandle(value); + } +}; +template +class return_wrapper> +{ +public: + static inline auto toReturnValue(const QJniObject &value) + { + return Jni::Array(value); + } +}; +template<> +struct return_wrapper { +}; +} +///@endcond +} + +#endif diff --git a/src/kandroidextras/jni/jnisignature.h b/src/kandroidextras/jni/jnisignature.h new file mode 100644 index 000000000..beaa57713 --- /dev/null +++ b/src/kandroidextras/jni/jnisignature.h @@ -0,0 +1,190 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNISIGNATURE_H +#define KANDROIDEXTRAS_JNISIGNATURE_H + +#include "jnitypes.h" + +#include "jni.h" +#include +#include + +namespace KAndroidExtras +{ + +namespace Jni +{ +template +class Array; +} + +/** @cond internal */ +namespace Internal +{ + +/** Compile-time concat-able string. */ +template +struct StaticString { + inline operator const char *() const + { + static const char data[] = {String..., 0}; + return data; + } +}; + +/** Compile-time strlen. */ +constexpr inline int static_strlen(const char *str) +{ + return str[0] == '\0' ? 0 : static_strlen(str + 1) + 1; +} + +/** Compile-time concat for two StaticString. */ +template +constexpr inline auto static_concat(const StaticString &, const StaticString &) +{ + return StaticString(); +} + +/** Compile-time concept for N StaticString. */ +template +constexpr inline auto static_concat(const String1 &str1, const String2 &str2, const Strings &...strs) +{ + return static_concat(static_concat(str1, str2), strs...); +} + +/** Conversion from const char* literals to StaticString. */ +template +struct staticStringFromJniType; +template +struct staticStringFromJniType> { + typedef StaticString()[Indexes]...> value; +}; + +/** Meta function for assembling JNI signatures. */ +template +struct JniSignature { + constexpr inline auto operator()() const + { + using namespace Internal; + return static_concat(StaticString<'L'>(), + typename staticStringFromJniType())>>::value(), + StaticString<';'>()); + } +}; + +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'Z'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'B'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'C'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'S'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'I'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'J'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'F'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'D'>(); + } +}; +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'V'>(); + } +}; +// special case due to jboolean != bool +template<> +struct JniSignature { + constexpr inline auto operator()() const + { + return StaticString<'Z'>(); + } +}; + +template +struct JniSignature { + constexpr inline auto operator()() const + { + using namespace Internal; + return static_concat(StaticString<'['>(), JniSignature()()); + } +}; + +template +struct JniSignature> { + constexpr inline auto operator()() const + { + using namespace Internal; + return static_concat(StaticString<'['>(), JniSignature()()); + } +}; + +template +struct JniSignature { + constexpr inline auto operator()() const + { + using namespace Internal; + return static_concat(StaticString<'('>(), JniSignature()()..., StaticString<')'>(), JniSignature()()); + } +}; +} +/** @endcond */ + +/** Helper methods to deal with JNI. */ +namespace Jni +{ +/** Returns the JNI signature string for the template argument types. */ +template +constexpr __attribute__((__unused__)) Internal::JniSignature signature = {}; +} + +} + +#endif // KANDROIDEXTRAS_JNISIGNATURE_H diff --git a/src/kandroidextras/jni/jnitypes.h b/src/kandroidextras/jni/jnitypes.h new file mode 100644 index 000000000..95f0e67c0 --- /dev/null +++ b/src/kandroidextras/jni/jnitypes.h @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2019 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNITYPES_H +#define KANDROIDEXTRAS_JNITYPES_H + +#include "jnipp.h" + +/** C++/Android integration utilities built on top of @c QJniObject. */ +namespace KAndroidExtras +{ + +///@cond internal + +// preprocessor "iteration" for regular classes +#define JNI_TYPE_1(name, type, ...) \ + struct type { \ + static constexpr const char *jniName() \ + { \ + return name #type; \ + } \ + }; +#define JNI_TYPE_2(name, type, ...) \ + namespace type \ + { \ + JNI_TYPE_1(name #type "/", __VA_ARGS__) \ + } +#define JNI_TYPE_3(name, type, ...) \ + namespace type \ + { \ + JNI_TYPE_2(name #type "/", __VA_ARGS__) \ + } +#define JNI_TYPE_4(name, type, ...) \ + namespace type \ + { \ + JNI_TYPE_3(name #type "/", __VA_ARGS__) \ + } +#define JNI_TYPE_5(name, type, ...) \ + namespace type \ + { \ + JNI_TYPE_4(name #type "/", __VA_ARGS__) \ + } +#define JNI_TYPE_6(name, type, ...) \ + namespace type \ + { \ + JNI_TYPE_5(name #type "/", __VA_ARGS__) \ + } +#define JNI_TYPE_7(name, type, ...) \ + namespace type \ + { \ + JNI_TYPE_6(#type "/", __VA_ARGS__) \ + } +#define JNI_TYPE_(N, name, ...) JNI_PP_CONCAT(JNI_TYPE_, N)(name, __VA_ARGS__) + +// preprocessor "iteration" for nested classes +#define JNI_NESTED_TYPE_2(name, type, nested_type, ...) \ + struct type##_##nested_type { \ + static constexpr const char *jniName() \ + { \ + return name #type "$" #nested_type; \ + } \ + }; +#define JNI_NESTED_TYPE_3(name, type, ...) \ + namespace type \ + { \ + JNI_NESTED_TYPE_2(name #type "/", __VA_ARGS__) \ + } +#define JNI_NESTED_TYPE_4(name, type, ...) \ + namespace type \ + { \ + JNI_NESTED_TYPE_3(name #type "/", __VA_ARGS__) \ + } +#define JNI_NESTED_TYPE_5(name, type, ...) \ + namespace type \ + { \ + JNI_NESTED_TYPE_4(name #type "/", __VA_ARGS__) \ + } +#define JNI_NESTED_TYPE_6(name, type, ...) \ + namespace type \ + { \ + JNI_NESTED_TYPE_5(name #type "/", __VA_ARGS__) \ + } +#define JNI_NESTED_TYPE_7(name, type, ...) \ + namespace type \ + { \ + JNI_NESTED_TYPE_6(#type "/", __VA_ARGS__) \ + } +#define JNI_NESTED_TYPE_(N, name, ...) JNI_PP_CONCAT(JNI_NESTED_TYPE_, N)(name, __VA_ARGS__) + +///@endcond + +/** Macro to define Java types with their corresponding JNI signature strings. */ +#define JNI_TYPE(...) JNI_TYPE_(JNI_PP_NARG(__VA_ARGS__), "", __VA_ARGS__) + +/** Macro to define a nested Java class with its corresponding JNI signature string. */ +#define JNI_NESTED_TYPE(...) JNI_NESTED_TYPE_(JNI_PP_NARG(__VA_ARGS__), "", __VA_ARGS__) + +/** Functions for interfacing with the Java Native Interface (JNI). */ +namespace Jni +{ +/** Returns the JNI type name of the given template argument. */ +template +inline constexpr const char *typeName() +{ + return T::jniName(); +} +} + +} + +#endif // KANDROIDEXTRAS_JNITYPES_H diff --git a/src/kandroidextras/jni/jnitypetraits.h b/src/kandroidextras/jni/jnitypetraits.h new file mode 100644 index 000000000..bccce289b --- /dev/null +++ b/src/kandroidextras/jni/jnitypetraits.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2020 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KANDROIDEXTRAS_JNITYPETRAITS_H +#define KANDROIDEXTRAS_JNITYPETRAITS_H + +#include "jniprimitivetypes.h" +#include "jnitypes.h" + +#include + +namespace KAndroidExtras +{ + +namespace Jni +{ + +template +class Array; + +/** Type conversion trait, see @c JNI_DECLARE_CONVERTER. */ +template +struct converter { + typedef void type; +}; + +template +struct reverse_converter { + typedef converter::type> type; +}; + +/** Type trait for checking whether @tparam T is a JNI array type. */ +template +struct is_array : std::false_type { +}; +template +struct is_array> : std::true_type { +}; + +/** Type trais for checking whether @tparam T is a JNI object wrapper type. */ +template> +struct is_object_wrapper : std::false_type { +}; +template +struct is_object_wrapper> : std::true_type { +}; + +/** Type trais for checking whether @tparam T is needs the generic JNI object wrapper (Jni::Object). */ +template +struct is_generic_wrapper + : std::conditional_t::value && !is_array::value && !is_object_wrapper::value, std::true_type, std::false_type> { +}; + +} + +/** + * Declare a JNI type to be convertible to a native type. + * @param JniType A type declared with @p JNI_TYPE. + * @param NativeType A C++ type @p JniType can be converted to/from. + * @param FromJniFn Code converting a @c QJniObject @c value to @p NativeType. + * @param ToJniCode converting a @p NativeType @c value to a QJniObject. + */ +#define JNI_DECLARE_CONVERTER(JniType, NativeType, FromJniFn, ToJniFn) \ + namespace Jni \ + { \ + template<> \ + struct converter { \ + typedef JniType type; \ + static inline QJniObject convert(const NativeType &value) \ + { \ + return (ToJniFn); \ + } \ + }; \ + template<> \ + struct converter { \ + typedef NativeType type; \ + static inline NativeType convert(const QJniObject &value) \ + { \ + return (FromJniFn); \ + } \ + }; \ + } + +} + +#endif diff --git a/src/notificationsmanager.cpp b/src/notificationsmanager.cpp index 7b7e80f11..4940df43a 100644 --- a/src/notificationsmanager.cpp +++ b/src/notificationsmanager.cpp @@ -23,6 +23,7 @@ #include "controller.h" #include "neochatconnection.h" #include "neochatroom.h" +#include "permissionmanager.h" #include "roommanager.h" #include "texthandler.h" #include "windowcontroller.h" @@ -42,6 +43,10 @@ NotificationsManager::NotificationsManager(QObject *parent) void NotificationsManager::handleNotifications(QPointer connection) { + if (!PermissionManager::instance().checkPermission(Permission::PostNotification)) { + return; + } + if (!m_connActiveJob.contains(connection->user()->id())) { auto job = connection->callApi(); m_connActiveJob.append(connection->user()->id()); diff --git a/src/permissionmanager.cpp b/src/permissionmanager.cpp new file mode 100644 index 000000000..c1e97c0a5 --- /dev/null +++ b/src/permissionmanager.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2022 Volker Krause + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "permissionmanager.h" + +#include + +#ifdef Q_OS_ANDROID +#include "private/qandroidextras_p.h" +#include +#endif + +#ifdef Q_OS_ANDROID +static QString permissionName(Permission::Permission p) +{ + using namespace KAndroidExtras; + switch (p) { + case Permission::InvalidPermission: + Q_UNREACHABLE(); + case Permission::ReadCalendar: + return ManifestPermission::READ_CALENDAR; + case Permission::WriteCalendar: + return ManifestPermission::WRITE_CALENDAR; + case Permission::PostNotification: // only for SDK >= 33 + return ManifestPermission::POST_NOTIFICATIONS; + case Permission::Camera: + return ManifestPermission::CAMERA; + } +} +#endif + +PermissionManager &PermissionManager::instance() +{ + static PermissionManager _instance; + return _instance; +} + +PermissionManager::PermissionManager(QObject *parent) + : QObject(parent) +{ +} + +bool PermissionManager::checkPermission(Permission::Permission permission) +{ + if (permission == Permission::InvalidPermission) { + qWarning() << "check for invalid permission - check your QML code!"; + return false; + } +#ifdef Q_OS_ANDROID + if (permission == Permission::PostNotification && QtAndroidPrivate::androidSdkVersion() < 33) { + return true; + } + + return QtAndroidPrivate::checkPermission(permissionName(permission)).result() == QtAndroidPrivate::PermissionResult::Authorized; +#else // non-Android + Q_UNUSED(permission); + return true; +#endif +} + +void PermissionManager::requestPermission(Permission::Permission permission, QJSValue callback) +{ + if (permission == Permission::InvalidPermission) { + qWarning() << "request for invalid permission - check your QML code!"; + return; + } + + qDebug() << permission; + // already got the permission + if (PermissionManager::checkPermission(permission)) { + callback.call(); + return; + } + +#ifdef Q_OS_ANDROID + // TODO make this properly async + if (QtAndroidPrivate::checkPermission(permissionName(permission)).result() != QtAndroidPrivate::PermissionResult::Authorized) { + if (QtAndroidPrivate::requestPermission(permissionName(permission)).result() != QtAndroidPrivate::PermissionResult::Authorized) { + return; + } + } + callback.call(); +#endif +} + +#include "moc_permissionmanager.cpp" diff --git a/src/permissionmanager.h b/src/permissionmanager.h new file mode 100644 index 000000000..caaf39506 --- /dev/null +++ b/src/permissionmanager.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Volker Krause +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include + +/** Permission enum for use in QML. */ +namespace Permission +{ +Q_NAMESPACE +enum Permission { + InvalidPermission, // captures QML errors resulting in "0" enum values + ReadCalendar, + WriteCalendar, + PostNotification, + Camera, +}; +Q_ENUM_NS(Permission) +} + +/** Check and request platform permissions for access to controlled resources (calendar, location, etc). */ +class PermissionManager : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON +public: + static PermissionManager &instance(); + static PermissionManager *create(QQmlEngine *engine, QJSEngine *) + { + engine->setObjectOwnership(&instance(), QQmlEngine::CppOwnership); + return &instance(); + } + + Q_INVOKABLE static bool checkPermission(Permission::Permission permission); + Q_INVOKABLE static void requestPermission(Permission::Permission permission, QJSValue callback); + +private: + explicit PermissionManager(QObject *parent = nullptr); +};