Merge pull request 'Android app proof-of-concept' (#48) from dsc/wowlet:android into master

Reviewed-on: #48
pull/53/head
dsc 3 years ago
commit 499ad4a3aa

@ -11,11 +11,13 @@ set(VERSION "beta-2")
option(FETCH_DEPS "Download dependencies if they are not found" ON)
option(XMRIG "Include XMRig module" ON)
option(OPENVR "Include OpenVR support" OFF)
option(QML "Include QtQuick (QML)" OFF)
option(TOR_BIN "Path to Tor binary to embed inside WOWlet" OFF)
option(OPENVR "Include OpenVR support")
option(QML "Include QtQuick (QML)")
option(ANDROID "Android deployment")
option(ANDROID_DEBUG "View the Android app on desktop")
option(TOR_BIN "Path to Tor binary to embed inside WOWlet")
option(STATIC "Link libraries statically, requires static Qt")
option(USE_DEVICE_TREZOR "Trezor support compilation" OFF)
option(USE_DEVICE_TREZOR "Trezor support compilation")
option(DONATE_BEG "Prompt donation window every once in a while" ON)
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_SOURCE_DIR}/cmake")
include(CheckCCompilerFlag)
@ -28,11 +30,11 @@ include(CheckSymbolExists)
set(WOWNERO_HEAD "f611d5c9e32bc62f1735f6571b0bdb95cc020531")
set(BUILD_GUI_DEPS ON)
set(ARCH "x86-64")
set(BUILD_64 ON)
set(ARCH "x86-64" CACHE STRING "Target architecture")
set(BUILD_64 ON CACHE BOOL "Build 64-bit binaries")
set(INSTALL_VENDORED_LIBUNBOUND ${STATIC})
set(USE_SINGLE_BUILDDIR ON)
if(OPENVR)
if(OPENVR OR ANDROID_DEBUG)
set(QML ON)
endif()
@ -41,10 +43,7 @@ set(_CMAKE_BUILD_TYPE "")
string(TOUPPER "${CMAKE_BUILD_TYPE}" _CMAKE_BUILD_TYPE)
if("${_CMAKE_BUILD_TYPE}" STREQUAL "DEBUG")
set(DEBUG ON)
set(CMAKE_VERBOSE_MAKEFILE ON)
message(STATUS "OPENVR: ${OPENVR}")
message(STATUS "QML: ${QML}")
endif()
check_include_file(sys/prctl.h HAVE_SYS_PRCTL_H)
@ -173,7 +172,7 @@ find_package(Boost 1.58 REQUIRED COMPONENTS
program_options
locale)
if(UNIX AND NOT APPLE)
if(UNIX AND NOT APPLE AND NOT ANDROID)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
# https://github.com/monero-project/monero-gui/issues/3142#issuecomment-705940446
set(CMAKE_SKIP_RPATH ON)
@ -195,6 +194,17 @@ if("$ENV{DRONE}" STREQUAL "true")
message(STATUS "We are inside a static compile with Drone CI")
endif()
if(UNIX)
if(NOT CMAKE_PREFIX_PATH AND DEFINED ENV{CMAKE_PREFIX_PATH})
message(STATUS "Using CMAKE_PREFIX_PATH environment variable: '$ENV{CMAKE_PREFIX_PATH}'")
set(CMAKE_PREFIX_PATH $ENV{CMAKE_PREFIX_PATH})
endif()
if(APPLE AND NOT CMAKE_PREFIX_PATH)
execute_process(COMMAND brew --prefix qt5 OUTPUT_VARIABLE QT5_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
list(APPEND CMAKE_PREFIX_PATH ${QT5_DIR})
endif()
endif()
# To build WOWlet with embedded (and static) Tor, pass CMake -DTOR_BIN=/path/to/tor
if(TOR_BIN)
if(APPLE)
@ -264,7 +274,7 @@ elseif(DRAGONFLY)
set(EXTRA_LIBRARIES execinfo ${COMPAT})
elseif(CMAKE_SYSTEM_NAME MATCHES "(SunOS|Solaris)")
set(EXTRA_LIBRARIES socket nsl resolv)
elseif(NOT MSVC AND NOT DEPENDS)
elseif(NOT MSVC AND NOT DEPENDS AND NOT ANDROID)
find_library(RT rt)
set(EXTRA_LIBRARIES ${RT})
endif()
@ -384,10 +394,6 @@ if(OPENVR)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/contrib/openvr")
endif()
if(APPLE)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/contrib/KDMacTouchBar")
endif()
if(WITH_SCANNER)
add_library(quirc STATIC
contrib/quirc/lib/decode.c

@ -0,0 +1,245 @@
FROM debian:stretch
ARG THREADS=1
ARG ANDROID_NDK_REVISION=21d
ARG ANDROID_NDK_HASH=bcf4023eb8cb6976a4c7cff0a8a8f145f162bf4d
ARG ANDROID_SDK_REVISION=4333796
ARG ANDROID_SDK_HASH=92ffee5a1d98d856634e8b71132e8a95d96c83a63fde1099be3d86df3106def9
ARG QT_VERSION=5.15.2
WORKDIR /opt/android
ENV WORKDIR=/opt/android
ENV ANDROID_NATIVE_API_LEVEL=28
ENV ANDROID_API=android-${ANDROID_NATIVE_API_LEVEL}
ENV ANDROID_CLANG=aarch64-linux-android${ANDROID_NATIVE_API_LEVEL}-clang
ENV ANDROID_CLANGPP=aarch64-linux-android${ANDROID_NATIVE_API_LEVEL}-clang++
ENV ANDROID_NDK_ROOT=${WORKDIR}/android-ndk-r${ANDROID_NDK_REVISION}
ENV ANDROID_SDK_ROOT=${WORKDIR}/tools
ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
ENV PATH=${JAVA_HOME}/bin:${PATH}
ENV PREFIX=${WORKDIR}/prefix
ENV TOOLCHAIN_DIR=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64
RUN apt-get update \
&& apt-get install -y ant automake build-essential ca-certificates-java file gettext git libc6 libncurses5 \
libssl-dev libstdc++6 libtinfo5 libtool libz1 openjdk-8-jdk-headless openjdk-8-jre-headless pkg-config python3 \
unzip wget
RUN wget -q https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_REVISION}.zip \
&& unzip -q sdk-tools-linux-${ANDROID_SDK_REVISION}.zip \
&& rm -f sdk-tools-linux-${ANDROID_SDK_REVISION}.zip
RUN wget -q https://dl.google.com/android/repository/android-ndk-r${ANDROID_NDK_REVISION}-linux-x86_64.zip \
&& unzip -q android-ndk-r${ANDROID_NDK_REVISION}-linux-x86_64.zip \
&& rm -f android-ndk-r${ANDROID_NDK_REVISION}-linux-x86_64.zip
RUN cd ${ANDROID_SDK_ROOT} && echo y | ./bin/sdkmanager "platform-tools" "platforms;${ANDROID_API}" "tools" > /dev/null
RUN cp -r ${WORKDIR}/platforms ${WORKDIR}/platform-tools ${ANDROID_SDK_ROOT}
ENV HOST_PATH=${PATH}
ENV PATH=${TOOLCHAIN_DIR}/aarch64-linux-android/bin:${TOOLCHAIN_DIR}/bin:${PATH}
ARG ZLIB_VERSION=1.2.11
ARG ZLIB_HASH=c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1
RUN wget -q https://zlib.net/zlib-${ZLIB_VERSION}.tar.gz \
&& tar -xzf zlib-${ZLIB_VERSION}.tar.gz \
&& rm zlib-${ZLIB_VERSION}.tar.gz \
&& cd zlib-${ZLIB_VERSION} \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --prefix=${PREFIX} --static \
&& make -j${THREADS} \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
RUN git clone git://code.qt.io/qt/qt5.git -b ${QT_VERSION} --depth 1 \
&& cd qt5 \
&& perl init-repository --module-subset=default,-qtwebengine \
&& PATH=${HOST_PATH} ./configure -v -developer-build -release \
-xplatform android-clang \
-android-ndk-platform ${ANDROID_API} \
-android-ndk ${ANDROID_NDK_ROOT} \
-android-sdk ${ANDROID_SDK_ROOT} \
-android-ndk-host linux-x86_64 \
-no-dbus \
-opengl es2 \
-no-use-gold-linker \
-no-sql-mysql \
-opensource -confirm-license \
-android-arch arm64-v8a \
-prefix ${PREFIX} \
-nomake tools -nomake tests -nomake examples \
-skip qtwebengine \
-skip qtserialport \
-skip qtconnectivity \
-skip qttranslations \
-skip qtpurchasing \
-skip qtgamepad -skip qtscript -skip qtdoc \
-no-warnings-are-errors \
&& sed -i '213,215d' qtbase/src/3rdparty/pcre2/src/sljit/sljitConfigInternal.h \
&& PATH=${HOST_PATH} make -j${THREADS} \
&& PATH=${HOST_PATH} make -j${THREADS} install \
&& cd qttools/src/linguist/lrelease \
&& ../../../../qtbase/bin/qmake \
&& PATH=${HOST_PATH} make -j${THREADS} install \
&& cd ../../../.. \
&& rm -rf $(pwd)
ARG ICONV_VERSION=1.16
ARG ICONV_HASH=e6a1b1b589654277ee790cce3734f07876ac4ccfaecbee8afa0b649cf529cc04
RUN wget -q http://ftp.gnu.org/pub/gnu/libiconv/libiconv-${ICONV_VERSION}.tar.gz \
&& echo "${ICONV_HASH} libiconv-${ICONV_VERSION}.tar.gz" | sha256sum -c \
&& tar -xzf libiconv-${ICONV_VERSION}.tar.gz \
&& rm -f libiconv-${ICONV_VERSION}.tar.gz \
&& cd libiconv-${ICONV_VERSION} \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --build=x86_64-linux-gnu --host=aarch64 --prefix=${PREFIX} --disable-rpath \
&& make -j${THREADS} \
&& make -j${THREADS} install
ARG BOOST_VERSION=1_74_0
ARG BOOST_VERSION_DOT=1.74.0
ARG BOOST_HASH=83bfc1507731a0906e387fc28b7ef5417d591429e51e788417fe9ff025e116b1
RUN wget -q https://dl.bintray.com/boostorg/release/${BOOST_VERSION_DOT}/source/boost_${BOOST_VERSION}.tar.bz2 \
&& echo "${BOOST_HASH} boost_${BOOST_VERSION}.tar.bz2" | sha256sum -c \
&& tar -xf boost_${BOOST_VERSION}.tar.bz2 \
&& rm -f boost_${BOOST_VERSION}.tar.bz2 \
&& cd boost_${BOOST_VERSION} \
&& PATH=${HOST_PATH} ./bootstrap.sh --prefix=${PREFIX} \
&& PATH=${TOOLCHAIN_DIR}/bin:${HOST_PATH} ./b2 --build-type=minimal link=static runtime-link=static \
--with-chrono --with-date_time --with-filesystem --with-program_options --with-regex --with-serialization \
--with-system --with-thread --with-locale --build-dir=android --stagedir=android toolset=clang threading=multi \
threadapi=pthread target-os=android -sICONV_PATH=${PREFIX} \
cflags='--target=aarch64-linux-android' \
cxxflags='--target=aarch64-linux-android' \
linkflags='--target=aarch64-linux-android --sysroot=${ANDROID_NDK_ROOT}/platforms/${ANDROID_API}/arch-arm64 ${ANDROID_NDK_ROOT}/sources/cxx-stl/llvm-libc++/libs/arm64-v8a/libc++_shared.so -nostdlib++' \
install -j${THREADS} \
&& rm -rf $(pwd)
ARG OPENSSL_VERSION=1.1.1g
ARG OPENSSL_HASH=ddb04774f1e32f0c49751e21b67216ac87852ceb056b75209af2443400636d46
RUN wget -q https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz \
&& tar -xzf openssl-${OPENSSL_VERSION}.tar.gz \
&& rm openssl-${OPENSSL_VERSION}.tar.gz \
&& cd openssl-${OPENSSL_VERSION} \
&& ANDROID_NDK_HOME=${ANDROID_NDK_ROOT} ./Configure CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} \
android-arm64 no-asm no-shared --static \
--with-zlib-include=${PREFIX}/include --with-zlib-lib=${PREFIX}/lib \
--prefix=${PREFIX} --openssldir=${PREFIX} \
&& sed -i 's/CNF_EX_LIBS=-ldl -pthread//g;s/BIN_CFLAGS=-pie $(CNF_CFLAGS) $(CFLAGS)//g' Makefile \
&& ANDROID_NDK_HOME=${ANDROID_NDK_ROOT} make -j${THREADS} \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
ARG ZMQ_VERSION=v4.3.3
ARG ZMQ_HASH=04f5bbedee58c538934374dc45182d8fc5926fa3
RUN git clone https://github.com/zeromq/libzmq.git -b ${ZMQ_VERSION} --depth 1 \
&& cd libzmq \
&& git checkout ${ZMQ_HASH} \
&& ./autogen.sh \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --prefix=${PREFIX} --host=aarch64-linux-android \
--enable-static --disable-shared \
&& make -j${THREADS} \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
ARG SODIUM_VERSION=1.0.18
ARG SODIUM_HASH=4f5e89fa84ce1d178a6765b8b46f2b6f91216677
RUN set -ex \
&& git clone https://github.com/jedisct1/libsodium.git -b ${SODIUM_VERSION} --depth 1 \
&& cd libsodium \
&& test `git rev-parse HEAD` = ${SODIUM_HASH} || exit 1 \
&& ./autogen.sh \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --prefix=${PREFIX} --host=aarch64-linux-android --enable-static --disable-shared \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
RUN git clone -b libgpg-error-1.38 --depth 1 git://git.gnupg.org/libgpg-error.git \
&& cd libgpg-error \
&& git reset --hard 71d278824c5fe61865f7927a2ed1aa3115f9e439 \
&& ./autogen.sh \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --host=aarch64-linux-android --prefix=${PREFIX} --disable-rpath --disable-shared --enable-static --disable-doc --disable-tests \
&& PATH=${TOOLCHAIN_DIR}/bin:${HOST_PATH} make -j${THREADS} \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
RUN git clone -b libgcrypt-1.8.5 --depth 1 git://git.gnupg.org/libgcrypt.git \
&& cd libgcrypt \
&& git reset --hard 56606331bc2a80536db9fc11ad53695126007298 \
&& ./autogen.sh \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --host=aarch64-linux-android --prefix=${PREFIX} --with-gpg-error-prefix=${PREFIX} --disable-shared --enable-static --disable-doc --disable-tests \
&& PATH=${TOOLCHAIN_DIR}/bin:${HOST_PATH} make -j${THREADS} \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
RUN cd tools \
&& wget -q http://dl-ssl.google.com/android/repository/tools_r25.2.5-linux.zip \
&& unzip -q tools_r25.2.5-linux.zip \
&& rm -f tools_r25.2.5-linux.zip \
&& echo y | ${ANDROID_SDK_ROOT}/tools/android update sdk --no-ui --all --filter build-tools-28.0.3
RUN git clone -b v3.19.7 --depth 1 https://github.com/Kitware/CMake \
&& cd CMake \
&& git reset --hard 22612dd53a46c7f9b4c3f4b7dbe5c78f9afd9581 \
&& PATH=${HOST_PATH} ./bootstrap \
&& PATH=${HOST_PATH} make -j${THREADS} \
&& PATH=${HOST_PATH} make -j${THREADS} install \
&& rm -rf $(pwd)
RUN git clone -b v1.6.35 --depth 1 https://github.com/glennrp/libpng.git && \
cd libpng && \
git reset --hard c17d164b4467f099b4484dfd4a279da0bc1dbd4a \
&& CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} ./configure --with-zlib-prefix="${PREFIX}" --host=aarch64-linux-android --prefix=${PREFIX} --disable-shared --enable-static \
&& PATH=${TOOLCHAIN_DIR}/bin:${HOST_PATH} make -j${THREADS} \
&& make -j${THREADS} install \
&& rm -rf $(pwd)
# @TODO: don't hardcode ANDROID_PLATFORM
RUN git clone -b v4.0.2 --depth 1 https://github.com/fukuchi/libqrencode.git && \
cd libqrencode && \
git reset --hard 59ee597f913fcfda7a010a6e106fbee2595f68e4 && \
CC=${ANDROID_CLANG} CXX=${ANDROID_CLANGPP} cmake \
-DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake" \
-DANDROID_PLATFORM="28" \
-DBUILD_SHARED_LIBS=OFF \
-DARCH="armv8-a" \
-DANDROID_ABI="arm64-v8a" \
-DANDROID_TOOLCHAIN=clang \
-DCMAKE_PREFIX_PATH="${PREFIX}" \
-DPNG_PNG_INCLUDE_DIR="${PREFIX}/include/libpng16/" \
-DPNG_LIBRARY="${PREFIX}/lib/libqtlibpng_arm64-v8a.a" \
-DICONV_LIBRARY=/opt/android/prefix/lib/libiconv.a \
-DICONV_INCLUDE_DIR=/opt/android/prefix/include/ \
-DCMAKE_INSTALL_PREFIX="${PREFIX}" && \
make -j$THREADS && \
make -j$THREADS install && \
rm -rf $(pwd)
RUN ls -al && uname -a
# @TODO: switch to Release
CMD set -ex \
&& cd /wowlet \
&& mkdir -p build/Android/release \
&& cd build/Android/release \
&& E=1 cmake \
-DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake" \
-DCMAKE_PREFIX_PATH="${PREFIX}" \
-DCMAKE_FIND_ROOT_PATH="${PREFIX}" \
-DCMAKE_BUILD_TYPE=Release \
-DARCH="armv8-a" \
-DANDROID_NATIVE_API_LEVEL=${ANDROID_NATIVE_API_LEVEL} \
-DANDROID_ABI="arm64-v8a" \
-DANDROID_TOOLCHAIN=clang \
-DBoost_USE_STATIC_RUNTIME=ON \
-DLRELEASE_PATH="${PREFIX}/bin" \
-DQT_ANDROID_APPLICATION_BINARY="wowlet" \
-DWITH_SCANNER=ON \
-DUSE_DEVICE_TREZOR=OFF \
-DUSE_SINGLE_BUILDDIR=ON \
-DMANUAL_SUBMODULES=1 \
-DUSE_SINGLE_BUILDDIR=ON \
-DQML=ON \
-DANDROID=ON \
../../.. \
&& PATH=${HOST_PATH} make generate_translations_header \
&& make -j${THREADS} -C src \
&& make -j${THREADS} apk

@ -28,7 +28,7 @@ by running this command: `pandoc wowlet.1.md -s -t man -o wowlet.1 && gzip wowle
apt install -y git cmake libqrencode-dev build-essential cmake libboost-all-dev \
miniupnpc libunbound-dev graphviz doxygen libunwind8-dev pkg-config libssl-dev \
libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev \
libprotobuf-dev protobuf-compiler libgcrypt20-dev
libprotobuf-dev protobuf-compiler libgcrypt20-dev libpng-dev
```
## Mac OS
@ -107,6 +107,4 @@ To skip the wizards and open a wallet directly use `--wallet-file`:
./wowlet --use-local-tor --wallet-file /home/user/Wownero/wallets/bla.keys
```
It is recommended that you use `--stagenet` for development. Testnet is also possible,
but you'll have to provide Wownero a testnet node of your own.

@ -57,6 +57,15 @@ if(OPENVR)
list(APPEND SOURCE_FILES ${SOURCE_FILES_QML})
endif()
if(ANDROID OR ANDROID_DEBUG)
qt5_add_resources(RESOURCES mobile/qml.qrc)
file(GLOB SOURCE_FILES_QML
"mobile/*.h"
"mobile/*.cpp"
)
list(APPEND SOURCE_FILES ${SOURCE_FILES_QML})
endif()
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wno-deprecated-declarations") # @TODO: removeme
add_subdirectory(libwalletqt)
@ -98,11 +107,16 @@ if(APPLE)
list(APPEND RESOURCES ${ICON})
endif()
add_executable(wowlet ${EXECUTABLE_FLAG} main.cpp
if(NOT ANDROID)
add_executable(wowlet ${EXECUTABLE_FLAG} main.cpp
${SOURCE_FILES}
${RESOURCES}
${ASSETS_TOR}
)
)
else()
add_library(wowlet SHARED ${SOURCE_FILES} ${RESOURCES})
set_target_properties(wowlet PROPERTIES COMPILE_DEFINITIONS "ANDROID")
endif()
# mac os bundle
set_target_properties(wowlet PROPERTIES
@ -162,6 +176,14 @@ if(XMRIG)
target_compile_definitions(wowlet PRIVATE HAS_XMRIG=1)
endif()
if(ANDROID)
target_compile_definitions(wowlet PRIVATE HAS_ANDROID=1)
endif()
if(ANDROID_DEBUG)
target_compile_definitions(wowlet PRIVATE HAS_ANDROID_DEBUG=1)
endif()
if(OPENVR)
target_compile_definitions(wowlet PRIVATE HAS_OPENVR=1)
target_compile_definitions(wowlet PUBLIC VR_API_PUBLIC)
@ -220,6 +242,13 @@ else()
target_link_libraries(wowlet PUBLIC monero-seed::monero-seed)
endif()
if(ANDROID)
# yolo some hardcoded paths
target_include_directories(wowlet PUBLIC
/opt/android/prefix/include/QtAndroidExtras/
)
endif()
# Link Wownero core libraries
target_link_libraries(wowlet PUBLIC
wallet_merged
@ -261,6 +290,33 @@ else()
Qt5::WebSockets)
endif()
if(ANDROID)
# yolo some hardcoded paths
target_link_libraries(wowlet PUBLIC
/opt/android/prefix/lib/libQt5QuickTemplates2_arm64-v8a.so
/opt/android/prefix/lib/libQt5Quick_arm64-v8a.so
/opt/android/prefix/lib/libQt5QmlModels_arm64-v8a.so
/opt/android/prefix/lib/libQt5Qml_arm64-v8a.so
/opt/android/prefix/lib/libQt5Svg_arm64-v8a.so
/opt/android/prefix/lib/libQt5Widgets_arm64-v8a.so
/opt/android/prefix/lib/libQt5Gui_arm64-v8a.so
/opt/android/prefix/lib/libQt5Xml_arm64-v8a.so
/opt/android/prefix/lib/libQt5XmlPatterns_arm64-v8a.so
/opt/android/prefix/lib/libQt5Network_arm64-v8a.so
/opt/android/prefix/lib/libQt5Core_arm64-v8a.so
/opt/android/prefix/lib/libQt5VirtualKeyboard_arm64-v8a.so
/opt/android/prefix/lib/libQt5AndroidExtras_arm64-v8a.so
/opt/android/prefix/plugins/bearer/libplugins_bearer_qandroidbearer_arm64-v8a.so
GLESv2
log
z
jnigraphics
android
EGL
c++_shared
)
endif()
# Link random other stuff
target_link_libraries(wowlet PUBLIC
${ICU_LIBRARIES}
@ -294,7 +350,7 @@ if(OPENVR)
endif()
if(APPLE)
target_link_libraries(wowlet
target_link_libraries(wowlet PUBLIC
KDMacTouchBar
)
target_include_directories(wowlet
@ -329,3 +385,19 @@ endif()
install(TARGETS wowlet
DESTINATION ${CMAKE_INSTALL_PREFIX}
)
message(STATUS "=============================================")
message(STATUS "VERSION_MAJOR: ${VERSION_MAJOR}")
message(STATUS "VERSION_MINOR: ${VERSION_MINOR}")
message(STATUS "VERSION_REVISION: ${VERSION_REVISION}")
message(STATUS "STATIC: ${STATIC}")
message(STATUS "Include QtQuick (QML): ${QML}")
message(STATUS "VERSION: ${VERSION}")
message(STATUS "Include the XMRIG tab: ${XMRIG}")
message(STATUS "Include Valve's OpenVR library: ${OPENVR}")
message(STATUS "This build is for Android: ${ANDROID}")
message(STATUS "This build is for testing the Android app on desktop: ${ANDROID_DEBUG}")
message(STATUS "TOR_BIN: ${TOR_BIN}")
message(STATUS "DONATE_BEG: ${DONATE_BEG}")
message(STATUS "=============================================")

@ -30,77 +30,69 @@ AppContext::AppContext(QCommandLineParser *cmdargs) {
this->cmdargs = cmdargs;
AppContext::isQML = false;
// OS & env
#if defined(Q_OS_MAC)
this->isMac = true;
this->isTorSocks = qgetenv("DYLD_INSERT_LIBRARIES").indexOf("libtorsocks") >= 0;
#elif __ANDROID__
this->isAndroid = true;
#elif defined(Q_OS_LINUX)
this->isLinux = true;
this->isTorSocks = qgetenv("LD_PRELOAD").indexOf("libtorsocks") >= 0;
this->isTails = TailsOS::detect();
this->isWhonix = WhonixOS::detect();
#elif defined(Q_OS_WIN)
this->isWindows = true;
this->isTorSocks = false;
#endif
this->androidDebug = cmdargs->isSet("android-debug");
this->isTails = TailsOS::detect();
this->isWhonix = WhonixOS::detect();
//Paths
// Paths
this->pathGenericData = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
this->configRoot = QDir::homePath();
if (isTails) { // #if defined(PORTABLE)
QString portablePath = []{
QString appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
qDebug() << "Not an appimage, using currentPath()";
return QDir::currentPath() + "/.wowlet";
}
QFileInfo appImageDir(appImagePath);
return appImageDir.absoluteDir().path() + "/.wowlet";
}();
if (QDir().mkpath(portablePath)) {
this->configRoot = portablePath;
} else {
qCritical() << "Unable to create portable directory: " << portablePath;
}
}
this->accountName = Utils::getUnixAccountName();
this->homeDir = QDir::homePath();
this->configDirectory = QString("%1/.config/wowlet/").arg(this->configRoot);
this->configDirectoryVR = QString("%1%2").arg(this->configDirectory, "vr");
if (isTails) this->setupPathsTails();
QString walletDir = config()->get(Config::walletDirectory).toString();
if (walletDir.isEmpty()) {
#if defined(Q_OS_LINUX) or defined(Q_OS_MAC)
this->defaultWalletDir = QString("%1/Wownero/wallets").arg(this->configRoot);
this->defaultWalletDirRoot = QString("%1/Wownero").arg(this->configRoot);
#elif defined(Q_OS_WIN)
this->defaultWalletDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Wownero";
this->defaultWalletDirRoot = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
#endif
if(walletDir.isEmpty()) {
if (isAndroid && !androidDebug) setupPathsAndroid();
else if (isWindows) setupPathsWindows();
else if (isLinux || isMac) setupPathsUnix();
} else {
this->defaultWalletDir = walletDir;
this->defaultWalletDirRoot = walletDir;
}
#ifdef __ANDROID__
// can haz disk I/O?
QVector<QString> perms = {
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE"
};
Utils::androidAskPermissions(perms);
#endif
// Create wallet dirs
qDebug() << "creating " << defaultWalletDir;
if (!QDir().mkpath(defaultWalletDir))
qCritical() << "Unable to create dir: " << defaultWalletDir;
this->configDirectory = QString("%1/.config/wowlet/").arg(this->configRoot);
#if defined(Q_OS_UNIX)
if(!this->configDirectory.endsWith('/'))
this->configDirectory = QString("%1/").arg(this->configDirectory);
#endif
this->configDirectoryVR = QString("%1%2").arg(this->configDirectory, "vr");
// Create some directories
createConfigDirectory(this->configDirectory);
// if(this->cmdargs->isSet("stagenet"))
// this->networkType = NetworkType::STAGENET;
// else if(this->cmdargs->isSet("testnet"))
// this->networkType = NetworkType::TESTNET;
// else
this->networkType = NetworkType::MAINNET;
qDebug() << "configRoot: " << this->configRoot;
qDebug() << "homeDir: " << this->homeDir;
qDebug() << "customWalletDir: " << walletDir;
qDebug() << "defaultWalletDir: " << this->defaultWalletDir;
qDebug() << "defaultWalletDirRoot: " << this->defaultWalletDirRoot;
qDebug() << "configDirectory: " << this->configDirectory;
// auto nodeSourceUInt = config()->get(Config::nodeSource).toUInt();
// AppContext::nodeSource = static_cast<NodeSource>(nodeSourceUInt);
this->nodes = new Nodes(this, this->networkClearnet);
@ -558,12 +550,14 @@ void AppContext::createConfigDirectory(const QString &dir) {
}
}
#ifdef HAS_OPENVR
auto config_dir_vr = QString("%1%2").arg(dir, "vr");
if(!Utils::dirExists(config_dir_vr)) {
qDebug() << QString("Creating directory: %1").arg(config_dir_vr);
if (!QDir().mkpath(config_dir_vr))
throw std::runtime_error("Could not create directory " + config_dir_vr.toStdString());
}
#endif
}
void AppContext::createWalletWithoutSpecifyingSeed(const QString &name, const QString &password) {
@ -949,3 +943,38 @@ void AppContext::refreshModels() {
this->currentWallet->coins()->refresh(this->currentWallet->currentSubaddressAccount());
// Todo: set timer for refreshes
}
void AppContext::setupPathsUnix() {
this->defaultWalletDir = QString("%1/Wownero/wallets").arg(this->configRoot);
this->defaultWalletDirRoot = QString("%1/Wownero").arg(this->configRoot);
}
void AppContext::setupPathsWindows() {
this->defaultWalletDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Wownero";
this->defaultWalletDirRoot = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
void AppContext::setupPathsAndroid() {
this->defaultWalletDir = QString("%1/Wownero/wallets").arg(this->pathGenericData);
this->defaultWalletDirRoot = QString("%1/Wownero").arg(this->pathGenericData);
}
void AppContext::setupPathsTails() {
QString portablePath = []{
QString appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
qDebug() << "Not an appimage, using currentPath()";
return QDir::currentPath() + "/.wowlet";
}
QFileInfo appImageDir(appImagePath);
return appImageDir.absoluteDir().path() + "/.wowlet";
}();
if (QDir().mkpath(portablePath)) {
this->configRoot = portablePath;
} else {
qCritical() << "Unable to create portable directory: " << portablePath;
}
}

@ -39,7 +39,12 @@ public:
~AppContext() override;
bool isTails = false;
bool isWhonix = false;
bool isAndroid = false;
bool isLinux = false;
bool isMac = false;
bool isWindows = false;
bool isDebug = false;
bool androidDebug = false;
// Donation config
const QString donationAddress = "Wo3MWeKwtA918DU4c69hVSNgejdWFCRCuWjShRY66mJkU2Hv58eygJWDJS1MNa2Ge5M1WjUkGHuLqHkweDxwZZU42d16v94mP";
@ -50,6 +55,7 @@ public:
QString coinName = "wownero";
bool isTorSocks = false;
QString pathGenericData;
QString homeDir;
QString accountName;
QString configRoot;
@ -215,6 +221,11 @@ private:
WalletKeysFilesModel *m_walletKeysFilesModel;
const int m_donationBoundary = 15;
QTimer m_storeTimer;
void setupPathsUnix();
void setupPathsWindows();
void setupPathsAndroid();
void setupPathsTails();
};
#endif //WOWLET_APPCONTEXT_H

@ -1,6 +1,8 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2014-2021, The Monero Project.
#include <thread>
#include "Wallet.h"
#include "TransactionHistory.h"

@ -14,6 +14,12 @@
#include "vr/main.h"
#endif
#ifdef HAS_ANDROID_DEBUG
#include "mobile/main.h"
#elif HAS_ANDROID
#include "mobile/main.h"
#endif
#if defined(Q_OS_WIN)
#include <windows.h>
#endif
@ -44,6 +50,10 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
argv_ << QString::fromStdString(argv[i]);
}
QCoreApplication::setApplicationName("wowlet");
QCoreApplication::setOrganizationDomain("wownero.org");
QCoreApplication::setOrganizationName("wownero.org");
QCommandLineParser parser;
parser.setApplicationDescription("wowlet");
parser.addHelpOption();
@ -91,9 +101,12 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
QCommandLineOption openVROption(QStringList() << "openvr", "Start Wowlet OpenVR");
parser.addOption(openVROption);
QCommandLineOption openVRDebugOption(QStringList() << "openvr-debug", "Start the Wowlet VR interface without initializing OpenVR - for debugging purposes.");
QCommandLineOption openVRDebugOption(QStringList() << "openvr-debug", "Start the Wowlet VR interface without initializing OpenVR - for debugging purposes. Requires -DOPENVR=ON CMake definition.");
parser.addOption(openVRDebugOption);
QCommandLineOption androidDebugOption(QStringList() << "android-debug", "Start the Android interface without actually running on Android - for debugging purposes. Requires -DANDROID_DEBUG=ON CMake definition.");
parser.addOption(androidDebugOption);
auto parsed = parser.parse(argv_);
if(!parsed) {
qCritical() << parser.errorText();
@ -111,13 +124,29 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
bool backgroundAddressEnabled = parser.isSet(backgroundOption);
bool openVREnabled = parser.isSet(openVROption);
bool cliMode = exportContacts || exportTxHistory || backgroundAddressEnabled;
bool androidDebug = parser.isSet(androidDebugOption);
bool android = false;
#ifdef __ANDROID__
android = true;
#endif
qRegisterMetaType<QVector<QString>>();
#ifdef HAS_QML
qputenv("QML_DISABLE_DISK_CACHE", "1");
#endif
if(android || androidDebug) {
#ifndef HAS_QML
qCritical() << "Wowlet compiled without QML support. Try -DQML=ON";
return 1;
#endif
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication mobile_app(argc, argv);
auto *ctx = new AppContext(&parser);
auto *mobile = new mobile::Mobile(ctx, &parser, &mobile_app);
return mobile_app.exec();
}
if(openVREnabled) {
#ifdef HAS_OPENVR
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
@ -138,9 +167,6 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
if(cliMode) {
auto *ctx = new AppContext(&parser);
QCoreApplication cli_app(argc, argv);
QCoreApplication::setApplicationName("wowlet");
QCoreApplication::setOrganizationDomain("wownero.org");
QCoreApplication::setOrganizationName("wownero.org");
ctx->applicationPath = QString(argv[0]);
ctx->isDebug = debugMode;
@ -191,10 +217,6 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
QApplication app(argc, argv);
QApplication::setApplicationName("wowlet");
QApplication::setOrganizationDomain("wownero.org");
QApplication::setOrganizationName("wownero.org");
parser.process(app); // Parse again for --help and --version
if(!quiet) {
@ -204,8 +226,10 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
if (stagenet) info["Mode"] = "Stagenet";
else if (testnet) info["Mode"] = "Testnet";
else info["Mode"] = "Mainnet";
#ifndef QT_NO_SSL
info["SSL"] = QSslSocket::sslLibraryVersionString();
info["SSL build"] = QSslSocket::sslLibraryBuildVersionString();
#endif
for (const auto &k: info.keys())
qWarning().nospace().noquote() << QString("%1: %2").arg(k).arg(info[k]);
}

@ -0,0 +1,45 @@
# Wowlet Mobile (Android)
This directory contains an unfinished QML application that will show:
![https://i.imgur.com/yhCsSgj.jpg](https://i.imgur.com/yhCsSgj.jpg)
## Building
Credits go to Monero GUI team for providing the initial work on Qt5+Android.
Build a Docker image:
```bash
docker build --tag wowlet:android --build-arg THREADS=14 --file Dockerfile.android .
```
Building Wowlet for arm64-v8a:
```Bash
docker run --rm -it -v $PWD:/wowlet -w /wowlet -e THREADS=6 wowlet:android
```
Installing the resulting `.apk` on your device:
```bash
adb install build/Android/release/android-build//build/outputs/apk/debug/android-build-debug.apk
```
Viewing debug logs:
```bash
adb logcat | grep --line-buffered "D wowlet"
```
# Development
To show this on desktop, you will need the following CMake definitions:
`-DANDROID_DEBUG=ON -DWITH_SCANNER=ON`
Start wowlet with the `--android-debug` flag:
```bash
./wowlet --android-debug
```

@ -0,0 +1,102 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include <iostream>
#include <QResource>
#include <QApplication>
#include <QCoreApplication>
#include <QQmlComponent>
#include <QObject>
#include <QtCore>
#include <QtGui>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QFileInfo>
#include <QQuickView>
#include <QQuickItem>
#include "libwalletqt/TransactionInfo.h"
#include "libwalletqt/TransactionHistory.h"
#include "model/TransactionHistoryModel.h"
#include "model/TransactionHistoryProxyModel.h"
#include "libwalletqt/WalletManager.h"
#include "utils/keysfiles.h"
#include "mobile/main.h"
namespace mobile {
Mobile::Mobile(AppContext *ctx, QCommandLineParser *parser, QObject *parent) :
QObject(parent), ctx(ctx), m_parser(parser) {
AppContext::isQML = true;
m_pClipboard = QGuiApplication::clipboard();
desktopMode = true;
// turn on auto tx commits
ctx->autoCommitTx = true;
// QR code scanning from screenshots
m_qrScreenshotPreviewPath = ctx->configDirectoryVR + "/screenshot_preview";
m_qrScreenshotImagePath = ctx->configDirectoryVR + "/screenshot";
m_qrScreenshotTimer.setSingleShot(true);
qDebug() << "QMLSCENE_DEVICE: " << qgetenv("QMLSCENE_DEVICE");
m_engine.rootContext()->setContextProperty("homePath", QDir::homePath());
m_engine.rootContext()->setContextProperty("applicationDirectory", QApplication::applicationDirPath());
m_engine.rootContext()->setContextProperty("idealThreadCount", QThread::idealThreadCount());
m_engine.rootContext()->setContextProperty("qtRuntimeVersion", qVersion());
m_engine.rootContext()->setContextProperty("ctx", ctx);
m_engine.rootContext()->setContextProperty("Mobile", this);
qRegisterMetaType<NetworkType::Type>();
qmlRegisterType<NetworkType>("wowlet.NetworkType", 1, 0, "NetworkType");
qmlRegisterUncreatableType<WalletKeysFiles>("wowlet.WalletKeysFiles", 1, 0, "WalletKeysFiles", "WalletKeysFiles can't be instantiated directly");
qmlRegisterUncreatableType<Wallet>("wowlet.Wallet", 1, 0, "Wallet", "Wallet can't be instantiated directly");
qmlRegisterType<WalletManager>("wowlet.WalletManager", 1, 0, "WalletManager");
qmlRegisterUncreatableType<TransactionHistoryProxyModel>("wowlet.TransactionHistoryProxyModel", 1, 0, "TransactionHistoryProxyModel", "TransactionHistoryProxyModel can't be instantiated directly");
qmlRegisterUncreatableType<TransactionHistoryModel>("wowlet.TransactionHistoryModel", 1, 0, "TransactionHistoryModel", "TransactionHistoryModel can't be instantiated directly");
qmlRegisterUncreatableType<TransactionInfo>("wowlet.TransactionInfo", 1, 0, "TransactionInfo", "TransactionHistory can't be instantiated directly");
qmlRegisterUncreatableType<TransactionHistory>("wowlet.TransactionHistory", 1, 0, "TransactionHistory", "TransactionHistory can't be instantiated directly");
qRegisterMetaType<PendingTransaction::Priority>();
qRegisterMetaType<TransactionInfo::Direction>();
qRegisterMetaType<TransactionHistoryModel::TransactionInfoRole>();
auto widgetUrl = QUrl(QStringLiteral("qrc:///main"));
m_engine.load(widgetUrl);
if (m_engine.rootObjects().isEmpty())
{
qCritical() << "Error: no root objects";
return;
}
QObject *rootObject = m_engine.rootObjects().first();
if (!rootObject)
{
qCritical() << "Error: no root objects";
return;
}
int wege = 1;
}
void Mobile::takeQRScreenshot() {
}
void Mobile::onCheckQRScreenshot() {
}
QString Mobile::checkQRScreenshotResults(std::vector<std::string> results) {
}
Mobile::~Mobile() {
// bla
int wegeg = 1;
}
}

@ -0,0 +1,92 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef WOWLET_MAIN_H
#define WOWLET_MAIN_H
#include <QtCore>
#include <QQmlError>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QGuiApplication>
#include <QClipboard>
#include <QTimer>
#include <globals.h>
#include "appcontext.h"
#include "utils/config.h"
#include "QR-Code-scanner/Decoder.h"
namespace mobile {
class Mobile : public QObject {
Q_OBJECT
public:
explicit Mobile(AppContext *ctx, QCommandLineParser *cmdargs, QObject *parent = nullptr);
~Mobile() override;
QList<QQmlError> errors;
Q_INVOKABLE double cdiv(double amount) { return amount / globals::cdiv; }
Q_INVOKABLE double add(double x, double y) const { return Utils::roundUp(x + y, 4); } // round ceil 4 decimals
Q_INVOKABLE double sub(double x, double y) const { return Utils::roundUp(x - y, 4); } // round ceil 4 decimals
Q_INVOKABLE void onCreateTransaction(const QString &address, const QString &amount_str, const QString description, bool all) {
auto amount = WalletManager::amountFromString(amount_str);
ctx->onCreateTransaction(address, amount, description, false);
}
Q_INVOKABLE void setClipboard(const QString &text) {
m_pClipboard->setText(text, QClipboard::Clipboard);
m_pClipboard->setText(text, QClipboard::Selection);
}
Q_INVOKABLE QString preferredFiat() {
return config()->get(Config::preferredFiatCurrency).toString();
}
Q_INVOKABLE QString fiatToWow(double amount) {
auto preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString();
if (amount <= 0) return QString("0.00");
double conversionAmount = AppContext::prices->convert(preferredFiatCurrency, "WOW", amount);
return QString("%1").arg(QString::number(conversionAmount, 'f', 2));
}
Q_INVOKABLE QString wowToFiat(double amount) {
auto preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString();
if (amount <= 0) return QString("0.00");
double conversionAmount = AppContext::prices->convert("WOW", preferredFiatCurrency, amount);
if(conversionAmount <= 0) return QString("0.00");
return QString("~%1").arg(QString::number(conversionAmount, 'f', 2));
}
Q_INVOKABLE void takeQRScreenshot();
signals:
void qrScreenshotFailed(QString error);
void qrScreenshotSuccess(QString address);
private slots:
void onCheckQRScreenshot();
private:
AppContext *ctx;
QQmlApplicationEngine m_engine;
bool desktopMode = false;
QString m_qrScreenshotPreviewPath;
QString m_qrScreenshotImagePath;
QCommandLineParser *m_parser;
QClipboard *m_pClipboard;
QTimer m_qrScreenshotTimer;
QrDecoder m_qrDecoder;
static QString checkQRScreenshotResults(std::vector<std::string> results);
};
}
#endif //WOWLET_MAIN_H

@ -0,0 +1,35 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
import QtQuick.Window 2.0
import QtQuick.Controls.Styles 1.4
import QtQuick.Dialogs 1.2
import QtGraphicalEffects 1.0
import "."
import wowlet.Wallet 1.0
import wowlet.WalletManager 1.0
ApplicationWindow {
visible: true
id: appWindow
width: 1080
height: 2400
color: "#2C3539"
MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}
Text {
text: "Wowlet"
color: "white"
anchors.centerIn: parent
font.pointSize: 62
}
}

@ -0,0 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
<file alias="main">main.qml</file>
</qresource>
</RCC>

@ -256,11 +256,14 @@ QStandardItem *Utils::qStandardItem(const QString& text, QFont &font) {
}
QString Utils::getUnixAccountName() {
#ifdef __ANDROID__
return "";
#endif
QString accountName = qgetenv("USER"); // mac/linux
if (accountName.isEmpty())
accountName = qgetenv("USERNAME"); // Windows
if (accountName.isEmpty())
throw std::runtime_error("Could derive system account name from env vars: USER or USERNAME");
throw std::runtime_error("Could not derive system account name from env vars: USER or USERNAME");
return accountName;
}
@ -455,3 +458,26 @@ QTextCharFormat Utils::addressTextFormat(const SubaddressIndex &index) {
}
return QTextCharFormat();
}
#ifdef __ANDROID__
bool Utils::androidAskPermissions(const QVector<QString> &permissions) {
bool rtn = true;
if(QtAndroid::androidSdkVersion() >= 23) {
for(const QString &permission : permissions) {
auto result = QtAndroid::checkPermission(permission);
if(result != QtAndroid::PermissionResult::Granted) {
auto resultHash = QtAndroid::requestPermissionsSync(QStringList({permission}));
if(resultHash[permission] != QtAndroid::PermissionResult::Granted) {
qDebug() << "Fail to get permission" << permission;
rtn = false;
} else {
qDebug() << "Permission" << permission << "granted!";
}
} else {
qDebug() << "Permission" << permission << "already granted!";
}
}
}
return rtn;
}
#endif

@ -8,6 +8,9 @@
#include <QStandardItemModel>
#include <QApplication>
#include <QTextCharFormat>
#ifdef __ANDROID__
#include <QtAndroid>
#endif
#include <monero_seed/monero_seed.hpp>
@ -80,10 +83,13 @@ public:
static QTextCharFormat addressTextFormat(const SubaddressIndex &index);
template<typename QEnum>
static QString QtEnumToString (const QEnum value)
{
static QString QtEnumToString (const QEnum value) {
return QString::fromStdString(std::string(QMetaEnum::fromType<QEnum>().valueToKey(value)));
}
#ifdef __ANDROID__
static bool androidAskPermissions(const QVector<QString> &permissions);
#endif
};
class AppContext; // forward declaration

Loading…
Cancel
Save