From 4239279cfd3be7e8dabd5b820b604ca63b34b4cd Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Sat, 1 Nov 2025 21:44:39 +0000 Subject: [PATCH] feat: add support for Android --- .github/workflows/build.yml | 90 ++++- .github/workflows/release.yml | 2 + CMakeLists.txt | 241 +++++++++-- README.md | 1 + cmake/modules/FindOboe.cmake | 71 ++++ cmake/modules/FindPortAudio.cmake | 11 + packaging/android/AndroidManifest.xml | 72 ++++ .../res/drawable/ic_launcher_background.xml | 38 ++ .../res/drawable/ic_launcher_foreground.xml | 382 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../android/src/org/mixxx/UsbPermission.java | 51 +++ src/controllers/android.cpp | 129 ++++++ src/controllers/android.h | 21 + src/controllers/bulk/bulkcontroller.cpp | 105 ++++- src/controllers/bulk/bulkcontroller.h | 12 + src/controllers/bulk/bulkenumerator.cpp | 78 +++- src/controllers/bulk/bulkenumerator.h | 2 + src/controllers/dlgprefcontroller.cpp | 6 +- src/controllers/dlgprefcontroller.h | 6 +- src/controllers/hid/hidcontroller.cpp | 77 +++- src/controllers/hid/hiddevice.cpp | 35 +- src/controllers/hid/hiddevice.h | 51 ++- src/controllers/hid/hidenumerator.cpp | 115 +++++- src/controllers/hid/hidenumerator.h | 1 + .../hid/hidioglobaloutputreportfifo.h | 2 +- src/controllers/hid/hidiothread.cpp | 13 +- src/controllers/hid/hidiothread.h | 12 + .../legacy/controllerscriptenginelegacy.cpp | 3 + src/coreservices.cpp | 41 ++ src/mixxxmainwindow.cpp | 2 +- src/preferences/configobject.cpp | 7 +- src/qml/qmlapplication.cpp | 20 +- src/soundio/sounddeviceportaudio.cpp | 25 +- src/soundio/soundmanager.cpp | 132 +++++- src/util/cmdlineargs.cpp | 4 +- src/util/desktophelper.cpp | 9 +- src/util/screensaver.cpp | 50 +++ src/util/screensaver.h | 2 + src/waveform/waveformwidgetfactory.cpp | 3 + src/widget/wspinnyglsl.cpp | 4 + tools/android_buildenv.sh | 139 +++++++ 41 files changed, 1977 insertions(+), 93 deletions(-) create mode 100644 cmake/modules/FindOboe.cmake create mode 100644 packaging/android/AndroidManifest.xml create mode 100644 packaging/android/res/drawable/ic_launcher_background.xml create mode 100644 packaging/android/res/drawable/ic_launcher_foreground.xml create mode 100644 packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 packaging/android/src/org/mixxx/UsbPermission.java create mode 100644 src/controllers/android.cpp create mode 100644 src/controllers/android.h create mode 100755 tools/android_buildenv.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 315377015fba..86833c57d4ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,10 @@ on: required: false MACOS_NOTARIZATION_APP_SPECIFIC_PASSWORD: required: false + ANDROID_SIGNING_KEYSTORE_BASE64: + required: false + ANDROID_SIGNING_PASSWORD: + required: false NETLIFY_BUILD_HOOK: required: false RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: @@ -173,11 +177,36 @@ jobs: artifacts_slug: windows-winarm qt_qpa_platform: windows arch: arm64 + - name: Android 15 arm64 + os: ubuntu-24.04 + # DBUILD_TESTING=OFF: error: OpenMP support and version of OpenMP (31, 40 or 45) differs + cmake_args: >- + -DBULK=ON + -DQT6=ON + -DQML=ON + -DHID=ON + -DVCPKG_TARGET_TRIPLET=arm64-android + -DVCPKG_DEFAULT_HOST_TRIPLET=x64-linux-release + -DCMAKE_SYSTEM_NAME=Android + -DBUILD_TESTING=OFF + -DBUILD_BENCH=OFF + buildenv_basepath: /home/runner/buildenv + buildenv_script: tools/android_buildenv.sh + artifacts_name: Android 15 APK + artifacts_path: build/android-build/build/outputs/apk/release/*.apk + artifacts_slug: android-15 + compiler_cache: ccache + compiler_cache_path: /home/runner/.cache/ccache + crosscompile: true + arch: arm64 env: # macOS codesigning MACOS_CODESIGN_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_P12_BASE64 }} MACOS_CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_PASSWORD }} + # Android signing + ANDROID_SIGNING_KEYSTORE_BASE64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }} + ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} runs-on: ${{ matrix.os }} name: ${{ matrix.name }} @@ -279,6 +308,33 @@ jobs: echo "CMAKE_ARGS_EXTRA=${CMAKE_ARGS_EXTRA} -DAPPLE_CODESIGN_IDENTITY=${APPLE_CODESIGN_IDENTITY}" >> "${GITHUB_ENV}" echo "APPLE_CODESIGN_IDENTITY=${APPLE_CODESIGN_IDENTITY}" >> $GITHUB_ENV + - name: "[android] Setup signing key" + if: startsWith(matrix.artifacts_slug, 'android') + run: | + if [ -z "${ANDROID_SIGNING_KEYSTORE_BASE64}" ]; then + # If no signing key is available (e.g running on a fork), generate a temporary key + keytool \ + -genkey \ + -keystore mixxx.keystore \ + -alias mixxx \ + -keyalg RSA \ + -keysize 2048 \ + -validity 365 \ + -keypass mixxxandroid \ + -storepass mixxxandroid \ + -dname "CN=${{ github.actor }}" + echo "QT_ANDROID_KEYSTORE_ALIAS=mixxx" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_KEY_PASS=mixxxandroid" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_STORE_PASS=mixxxandroid" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_PATH=${{ github.workspace }}/mixxx.keystore" >> $GITHUB_ENV + else + echo "${{ env.ANDROID_SIGNING_KEYSTORE_BASE64 }}" | base64 -d -o ${{ github.workspace }}/mixxx.keystore + echo "QT_ANDROID_KEYSTORE_ALIAS=mixxx" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_KEY_PASS=${{ env.ANDROID_SIGNING_PASSWORD }}" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_STORE_PASS=${{ env.ANDROID_SIGNING_PASSWORD }}" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_PATH=${{ github.workspace }}/mixxx.keystore" >> $GITHUB_ENV + fi + - name: "[macOS/Linux] Set up build environment" if: matrix.buildenv_script != null && runner.os != 'Windows' run: ${{ matrix.buildenv_script }} setup @@ -312,6 +368,29 @@ jobs: ${{ matrix.compiler_cache }} --max-size=2G if: runner.os != 'windows' + # Remove unused pre-installed software as the runner runs out of space otherwise + # Currently freeing up about 17.7G, ~20% + - name: "[android] Free up disk space" + if: startsWith(matrix.artifacts_slug, 'android') + run: | + sudo apt-get autoremove -y && sudo apt-get clean + sudo rm -rf /home/packer # Free up 677M + sudo rm -rf /opt/az # Free up 649M + sudo rm -rf /opt/google # Free up 378M + sudo rm -rf /opt/hostedtoolcache/CodeQL # Free up 1.6G + sudo rm -rf /opt/hostedtoolcache/go # Free up 808M + sudo rm -rf /opt/hostedtoolcache/node # Free up 532M + sudo rm -rf /opt/hostedtoolcache/PyPy # Free up 520M + sudo rm -rf /opt/hostedtoolcache/Python # Free up 1.5G + sudo rm -rf /opt/microsoft # Free up 781M + sudo rm -rf /opt/pipx # Free up 499M + sudo rm -rf /usr/lib/google-cloud-sdk # Free up 957M + sudo rm -rf /usr/local/julia1.11.7 # Free up 996M + sudo rm -rf /usr/local/share/powershell # Free up 1.3G + sudo rm -rf /usr/share/dotnet # Free up 3.4G + sudo rm -rf /usr/share/swift # Free up 3.2G + sudo rm -rf /usr/local/share/vcpkg # Size unknown, but obvious duplicate + - name: "Create build directory" run: mkdir build @@ -424,7 +503,7 @@ jobs: path: ${{ github.workspace }}/build/_CPack_Packages/win64/WIX/wix.log - name: "[Ubuntu] Import PPA GPG key" - if: startsWith(matrix.os, 'ubuntu') && env.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY != null + if: startsWith(matrix.os, 'ubuntu') && matrix.crosscompile != true && env.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY != null run: gpg --import <(echo "${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }}") env: RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: ${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }} @@ -496,6 +575,15 @@ jobs: --dest-url 'https://downloads.mixxx.org' ${{ matrix.artifacts_path }} + # TODO create a F-droid repo? + # - name: fdroid nightly + # run: | + # sudo add-apt-repository ppa:fdroid/fdroidserver + # sudo apt-get update + # sudo apt-get install apksigner fdroidserver --no-install-recommends + # export DEBUG_KEYSTORE=$ + # fdroid nightly --archive-older 10 + # Warning: do not move this step before restoring caches or it will break caching due to # https://github.com/actions/cache/issues/531 - name: "[Windows] Install rsync and openssh" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c38ef10995b..a2acd7f688fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,8 @@ jobs: MACOS_CODESIGN_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_P12_BASE64 }} MACOS_CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_PASSWORD }} MACOS_NOTARIZATION_APP_SPECIFIC_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_APP_SPECIFIC_PASSWORD }} + ANDROID_SIGNING_KEYSTORE_BASE64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }} + ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} NETLIFY_BUILD_HOOK: ${{ secrets.NETLIFY_BUILD_HOOK }} RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: ${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }} diff --git a/CMakeLists.txt b/CMakeLists.txt index d72891f687bb..89b9022f1048 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,11 @@ if(POLICY CMP0099) cmake_policy(SET CMP0099 NEW) endif() +# Add support for cmake_dependent_option +if(POLICY CMP0127) + cmake_policy(SET CMP0127 NEW) +endif() + # An imported target missing its location property fails during generation. if(POLICY CMP0111) cmake_policy(SET CMP0111 NEW) @@ -48,7 +53,54 @@ if(POLICY CMP0135) cmake_policy(SET CMP0135 NEW) endif() -if(((APPLE AND NOT IOS) OR WIN32) AND NOT IS_DIRECTORY "${MIXXX_VCPKG_ROOT}") +if(CMAKE_SYSTEM_NAME STREQUAL Android) + if(NOT DEFINED ENV{JAVA_HOME}) + message(FATAL_ERROR "JAVA_HOME is not set. Did you source the setup file?") + endif() + if((NOT CMAKE_ANDROID_NDK) AND DEFINED ENV{ANDROID_NDK_HOME}) + set(CMAKE_ANDROID_NDK "$ENV{ANDROID_NDK_HOME}") + endif() + set(ANDROID ON) + if(DEFINED ENV{QT_ANDROID_KEYSTORE_PATH}) + set(QT_ANDROID_SIGN_APK ON) + endif() + + set(QT_ANDROID_APP_PATH "$") + set( + QT_ANDROID_APP_PACKAGE_SOURCE_ROOT + "${CMAKE_SOURCE_DIR}/packaging/android" + ) + + if((NOT ANDROID_SDK_ROOT) AND DEFINED ENV{ANDROID_SDK}) + set(ANDROID_SDK_ROOT "$ENV{ANDROID_SDK}") + endif() + set(ANDROID_ABI arm64-v8a) + set(ANDROID_NDK_HOST_SYSTEM_NAME linux-x86_64) + set(CMAKE_SYSTEM_VERSION 35) # API level + set(ANDROID_PLATFORM "android-${CMAKE_SYSTEM_VERSION}") + set(ANDROID_API_VERSION "android-${CMAKE_SYSTEM_VERSION}") + set(CMAKE_ANDROID_ARCH_ABI "${ANDROID_ABI}") # or x86_64, armeabi-v7a, etc. + set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang) + set(CMAKE_ANDROID_STL_TYPE c++_shared) + set( + CMAKE_SYSROOT + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/sysroot" + ) + include_directories( + BEFORE + SYSTEM + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/sysroot/usr/include/" + ) + set( + CMAKE_LIBRARY_PATH + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/sysroot/usr/lib/aarch64-linux-android/${CMAKE_SYSTEM_VERSION}/;${CMAKE_LIBRARY_PATH}" + ) +endif() + +if( + ((APPLE AND NOT IOS) OR WIN32 OR ANDROID) + AND NOT IS_DIRECTORY "${MIXXX_VCPKG_ROOT}" +) if(NOT DEFINED BUILDENV_BASEPATH) if(DEFINED ENV{BUILDENV_BASEPATH}) set(BUILDENV_BASEPATH "$ENV{BUILDENV_BASEPATH}") @@ -172,7 +224,7 @@ function(fatal_error_missing_env) "Did you download the Mixxx build environment using `source ${CMAKE_SOURCE_DIR}/tools/macos_release_buildenv.sh setup` or `source ${CMAKE_SOURCE_DIR}/tools/macos_buildenv.sh setup` (includes Debug)?" ) endif() - elseif(UNIX AND NOT APPLE) + elseif(UNIX AND NOT APPLE AND NOT ANDROID) # Linux, BSD, Solaris, Minix if(EXISTS "/etc/debian_version") # exists also on Ubuntu and Mint message( @@ -284,8 +336,8 @@ set( # Set a default build type if none was specified # See https://blog.kitware.com/cmake-and-the-default-build-type/ for details. set(default_build_type "RelWithDebInfo") -if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND NOT WIN32) - # On Windows, Debug builds are linked to unoptimized libs +if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND NOT WIN32 AND NOT ANDROID) + # On Windows and Android, Debug builds are linked to unoptimized libs # generating unusable slow Mixxx builds. set(default_build_type "Debug") endif() @@ -946,7 +998,7 @@ else() else() message(STATUS "Could NOT find ccache (missing executable)") endif() - default_option(CCACHE_SUPPORT "Enable ccache support" "CCACHE_EXECUTABLE") + default_option(CCACHE_SUPPORT "Enable ccache support" "CCACHE_EXECUTABLE;NOT ANDROID") if(NOT DEFINED CMAKE_DISABLE_PRECOMPILE_HEADERS) set(CMAKE_DISABLE_PRECOMPILE_HEADERS ${CCACHE_SUPPORT}) @@ -1013,7 +1065,7 @@ if(NOT MSVC) set(MOLD_SYMLINK_FOUND TRUE) endif() default_option(MOLD_SUPPORT "Use 'mold' for linking" "MOLD_FUSE_FOUND OR MOLD_SYMLINK_FOUND") - if(MOLD_SUPPORT) + if(MOLD_SUPPORT AND NOT ANDROID) if(MOLD_FUSE_FOUND) message(STATUS "Selecting mold as linker") add_link_options("-fuse-ld=mold") @@ -2247,6 +2299,8 @@ if(WIN32) elseif(UNIX) if(APPLE) target_compile_definitions(mixxx-lib PUBLIC __APPLE__) + elseif(ANDROID) + target_compile_definitions(mixxx-lib PUBLIC __ANDROID__) else() target_compile_definitions(mixxx-lib PUBLIC __UNIX__) if(CMAKE_SYSTEM_NAME STREQUAL Linux) @@ -2268,7 +2322,92 @@ if(QT6) # below that takes care of the correct object order in the resulting binary # According to https://doc.qt.io/qt-6/qt-finalize-target.html it is importand for # builds with Qt < 3.21 - qt_add_executable(mixxx WIN32 src/main.cpp MANUAL_FINALIZATION) + if(ANDROID) + target_compile_definitions( + mixxx-lib + PUBLIC __STDC_CONSTANT_MACROS __STDC_LIMIT_MACROS __STDC_FORMAT_MACROS + ) + set_property(TARGET mixxx-lib PROPERTY ANDROID_ABIS "arm64-v8a") + target_compile_definitions( + mixxx-lib + PUBLIC ANDROID_PACKAGE_NAME="org.mixxx" + ) + qt_add_executable(mixxx src/main.cpp MANUAL_FINALIZATION) + set_property( + TARGET mixxx + PROPERTY + QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/packaging/android" + ) + set_target_properties(mixxx PROPERTIES QT_ANDROID_ABIS "${ANDROID_ABI}") + set_target_properties(mixxx PROPERTIES QT_ANDROID_PACKAGE_NAME "org.mixxx") + set_target_properties( + mixxx + PROPERTIES QT_ANDROID_VERSION_NAME "${MIXXX_VERSION}" + ) + set_target_properties( + mixxx + PROPERTIES + QT_ANDROID_APPLICATION_ARGUMENTS "--qml --log-level debug --developer" + ) + qt_add_android_permission(mixxx + NAME android.permission.ACCESS_NETWORK_STATE + ) + qt_add_android_permission(mixxx + NAME android.permission.INTERNET + ) + qt_add_android_permission(mixxx + NAME android.permission.MODIFY_AUDIO_SETTINGS + ) + qt_add_android_permission(mixxx + NAME android.permission.RECORD_AUDIO + ) + qt_add_android_permission(mixxx + NAME android.permission.WRITE_EXTERNAL_STORAGE + ) + qt_add_android_permission(mixxx + NAME android.permission.MANAGE_EXTERNAL_STORAGE + ) + qt_add_android_permission(mixxx + NAME android.permission.WAKE_LOCK + ) + qt_add_android_permission(mixxx + NAME android.permission.USB_PERMISSION + ) + set( + CMAKE_LINKER + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/bin/ld" + ) + set( + CMAKE_C_COMPILER + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/bin/clang" + ) + set( + CMAKE_CXX_COMPILER + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/bin/clang++" + ) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fPIC") + set_target_properties( + mixxx + PROPERTIES + QT_ANDROID_EXTRA_LIBS + ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/lib/clang/18/lib/linux/aarch64/libomp.so + ) + add_custom_target( + copy-resource-android + COMMENT "Copy resources folder to Android build asset directory" + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/packaging/android + ${CMAKE_CURRENT_BINARY_DIR}/android + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/res + ${CMAKE_CURRENT_BINARY_DIR}/android-build/assets + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ) + add_dependencies(mixxx copy-resource-android) + target_link_libraries(mixxx PUBLIC omp) + else() + qt_add_executable(mixxx WIN32 src/main.cpp MANUAL_FINALIZATION) + endif() else() find_package(Qt5 COMPONENTS Core) # For Qt Core cmake functions # This is the first package form the environment, if this fails give hints how to install the environment @@ -2357,37 +2496,41 @@ if(WIN32) include(InstallRequiredSystemLibraries) endif() -install( - TARGETS mixxx - RUNTIME DESTINATION "${MIXXX_INSTALL_BINDIR}" - BUNDLE DESTINATION . -) +if(NOT ANDROID) + install( + TARGETS mixxx + RUNTIME DESTINATION "${MIXXX_INSTALL_BINDIR}" + BUNDLE DESTINATION . + ) -# Skins -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/skins" - DESTINATION "${MIXXX_INSTALL_DATADIR}" -) + # Skins + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/skins" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + ) -# Controller mappings -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/controllers" - DESTINATION "${MIXXX_INSTALL_DATADIR}" -) + # Controller mappings + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/controllers" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + ) -# Effect presets -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/effects" - DESTINATION "${MIXXX_INSTALL_DATADIR}" -) + # Effect presets + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/effects" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + ) -# Translation files -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/translations" - DESTINATION "${MIXXX_INSTALL_DATADIR}" - FILES_MATCHING - PATTERN "*.qm" -) + # Translation files + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/translations" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + FILES_MATCHING + PATTERN "*.qm" + ) + # else() + # qt_finalize_target(mixxx-lib) +endif() # Font files # @@ -3433,6 +3576,10 @@ else() mixxx-lib PUBLIC -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2 -sFULL_ES2=1 ) + elseif(ANDROID) + find_library(GLESv2_LIBRARY GLESv2) + target_link_libraries(mixxx-lib PRIVATE "${GLESv2_LIBRARY}") + target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) else() find_package(WrapOpenGL REQUIRED) if(OPENGL_opengl_LIBRARY) @@ -3465,6 +3612,11 @@ target_link_libraries( find_package(PortAudio REQUIRED) target_link_libraries(mixxx-lib PUBLIC PortAudio::PortAudio) +if(ANDROID) + target_link_libraries(mixxx-lib PUBLIC OpenSLES android) + target_compile_definitions(mixxx-lib PUBLIC PA_USE_OBOE) +endif() + # PortAudio Ring Buffer add_library( PortAudioRingBuffer @@ -3476,7 +3628,7 @@ target_include_directories(mixxx-lib SYSTEM PUBLIC lib/portaudio) target_link_libraries(mixxx-lib PRIVATE PortAudioRingBuffer) # PortMidi -option(PORTMIDI "Enable the PortMidi backend for MIDI controllers" ON) +default_option(PORTMIDI "Enable the PortMidi backend for MIDI controllers" "NOT ANDROID") if(PORTMIDI) target_compile_definitions(mixxx-lib PUBLIC __PORTMIDI__) find_package(PortMidi REQUIRED) @@ -3567,6 +3719,10 @@ if(QT_EXTRA_COMPONENTS) endforeach() endif() +if(QT_KNOWN_POLICY_QTP0002 AND ANDROID) + qt6_policy(SET QTP0002 NEW) +endif() + if(QML) if(QT_KNOWN_POLICY_QTP0004) # See: https://doc.qt.io/qt-6/qt-cmake-policy-qtp0004.html @@ -4147,7 +4303,7 @@ if(APPLE) # Used for battery measurements and controlling the screensaver on macOS. target_link_libraries(mixxx-lib PRIVATE "-weak_framework IOKit") endif() -elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) +elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN AND NOT ANDROID) if(QT6) find_package(X11) else() @@ -4426,7 +4582,7 @@ cmake_dependent_option( BATTERY "Battery meter support" ON - "WIN32 OR UNIX" + "WIN32 OR (UNIX AND NOT ANDROID)" OFF ) if(BATTERY) @@ -4870,7 +5026,6 @@ if(HID) target_sources( mixxx-lib PRIVATE - src/controllers/controllerhidreporttabsmanager.cpp src/controllers/hid/hidcontroller.cpp src/controllers/hid/hidiothread.cpp src/controllers/hid/hidioglobaloutputreportfifo.cpp @@ -4882,6 +5037,16 @@ if(HID) src/controllers/hid/legacyhidcontrollermapping.cpp src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp ) + + if(ANDROID) + target_sources(mixxx-lib PRIVATE src/controllers/android.cpp) + else() + # Android doesn't support QWidget and is not able to compile this manager due to compiler lacking support for structure binding (error: capturing a structured binding is not yet supported) + target_sources( + mixxx-lib + PRIVATE src/controllers/controllerhidreporttabsmanager.cpp + ) + endif() target_compile_definitions(mixxx-lib PUBLIC __HID__) endif() diff --git a/README.md b/README.md index 828888e70ecb..8fa765250728 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ corresponding command for your operating system: | macOS | `source tools/macos_buildenv.sh setup` | ~1.5 GB download, ~3 GB disk space | | Debian/Ubuntu | `tools/debian_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | | Fedora | `tools/rpm_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | +| Android | `tools/android_buildenv.sh setup` (see the [wiki article](https://github.com/mixxxdj/mixxx/wiki/Building-for-Android)) | ~3.4 GB download, 13GB disk space | | Other Linux distros | See the [wiki article](https://github.com/mixxxdj/mixxx/wiki/Compiling%20on%20Linux) | | To build Mixxx, run diff --git a/cmake/modules/FindOboe.cmake b/cmake/modules/FindOboe.cmake new file mode 100644 index 000000000000..d12ef492639f --- /dev/null +++ b/cmake/modules/FindOboe.cmake @@ -0,0 +1,71 @@ +#[=======================================================================[.rst: +FindOboe +-------- + +Finds the Oboe library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``Oboe::Oboe`` + The Oboe library + +#]=======================================================================] + +# Prefer finding the libraries from pkgconfig rather than find_library. This is +# required to build with PipeWire's reimplementation of the Oboe library. +# +# This also enables using PortAudio with the Oboe port in vcpkg. That only +# builds OboeWeakAPI (not the Oboe server) which dynamically loads the real +# Oboe library and forwards API calls to it. OboeWeakAPI requires linking `dl` +# in addition to Oboe, as specified in the pkgconfig file in vcpkg. +find_package(PkgConfig QUIET) +if(PkgConfig_FOUND) + pkg_check_modules(Oboe Oboe) +endif() + +find_path( + Oboe_INCLUDE_DIR + NAMES oboe/Oboe.h + HINTS ${PC_Oboe_INCLUDE_DIRS} + DOC "Oboe include directory" +) +mark_as_advanced(Oboe_INCLUDE_DIR) + +find_library( + Oboe_LIBRARY + NAMES oboe + HINTS ${PC_Oboe_LIBRARY_DIRS} + DOC "Oboe library" +) +mark_as_advanced(Oboe_LIBRARY) + +if(DEFINED PC_Oboe_VERSION AND NOT PC_Oboe_VERSION STREQUAL "") + set(Oboe_VERSION "${PC_Oboe_VERSION}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Oboe + REQUIRED_VARS Oboe_LIBRARY Oboe_INCLUDE_DIR + VERSION_VAR Oboe_VERSION +) + +if(Oboe_FOUND) + set(Oboe_LIBRARIES "${Oboe_LIBRARY}") + set(Oboe_INCLUDE_DIRS "${Oboe_INCLUDE_DIR}") + set(Oboe_DEFINITIONS ${PC_Oboe_CFLAGS_OTHER}) + + if(NOT TARGET Oboe::Oboe) + add_library(Oboe::Oboe UNKNOWN IMPORTED) + set_target_properties( + Oboe::Oboe + PROPERTIES + IMPORTED_LOCATION "${Oboe_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${PC_Oboe_CFLAGS_OTHER}" + INTERFACE_INCLUDE_DIRECTORIES "${Oboe_INCLUDE_DIR}" + ) + endif() +endif() diff --git a/cmake/modules/FindPortAudio.cmake b/cmake/modules/FindPortAudio.cmake index cbd5377f51b1..c92beafe090a 100644 --- a/cmake/modules/FindPortAudio.cmake +++ b/cmake/modules/FindPortAudio.cmake @@ -110,6 +110,17 @@ if(PortAudio_FOUND) PROPERTY INTERFACE_LINK_LIBRARIES JACK::jack ) endif() + if(CMAKE_SYSTEM_NAME STREQUAL Android) + find_package(Oboe) + if(NOT (OBOE_FOUND)) + message(FATAL_ERROR "Oboe: not found") + endif() + set_property( + TARGET PortAudio::PortAudio + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES Oboe::Oboe + ) + endif() endif() if(PortAudio_ALSA_H) target_compile_definitions(PortAudio::PortAudio INTERFACE PA_USE_ALSA) diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml new file mode 100644 index 000000000000..f4822732c543 --- /dev/null +++ b/packaging/android/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/android/res/drawable/ic_launcher_background.xml b/packaging/android/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..9c3a415e54a1 --- /dev/null +++ b/packaging/android/res/drawable/ic_launcher_background.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/packaging/android/res/drawable/ic_launcher_foreground.xml b/packaging/android/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000000..248038dd13df --- /dev/null +++ b/packaging/android/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml b/packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000000..d378acd7ac99 --- /dev/null +++ b/packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/packaging/android/src/org/mixxx/UsbPermission.java b/packaging/android/src/org/mixxx/UsbPermission.java new file mode 100644 index 000000000000..731f6082083b --- /dev/null +++ b/packaging/android/src/org/mixxx/UsbPermission.java @@ -0,0 +1,51 @@ +package org.mixxx; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.util.Log; + +public class UsbPermission { + private static final String ACTION_USB_PERMISSION = + "org.mixxx.permissions.USB_PERMISSION"; + private static final String TAG = "MixxxUsbPermission"; + private static native void usbDeviceAccessResult(Object device, boolean granted); + public boolean registerServiceBroadcastReceiver(Context context) { + try { + IntentFilter intentFilter = new IntentFilter(ACTION_USB_PERMISSION); + context.registerReceiver(usbPermissionReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED); + Log.i(TAG, "Registered broadcast receiver"); + return true; + } catch (Exception e) { + Log.w(TAG, "Unable to register the broadcast receiver: " + e.toString()); + return false; + } + } + + private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.v(TAG, "Received " + action); + if (ACTION_USB_PERMISSION.equals(action)) { + synchronized (this) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (usbDevice == null) { + Log.e(TAG, "USB device is null"); + return; + } + boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + usbDeviceAccessResult(usbDevice, granted); + if (!granted) { + Log.w(TAG, "Permission was denied"); + } else { + Log.i(TAG, "Permission was granted"); + } + } + } + } + }; +} diff --git a/src/controllers/android.cpp b/src/controllers/android.cpp new file mode 100644 index 000000000000..0d5ade3b35aa --- /dev/null +++ b/src/controllers/android.cpp @@ -0,0 +1,129 @@ +#include "android.h" + +#include +#include +#include + +#include +#include + +namespace mixxx { +namespace android { +std::mutex s_androidLock = {}; +std::condition_variable s_grantingWaitCond = {}; +std::vector> s_grantingResult = {}; +QJniObject s_intent = {}; +QJniObject s_usbManager = {}; + +const QJniObject& getIntent() { + __android_log_print(ANDROID_LOG_VERBOSE, "mixxx", "about to get intent"); + std::unique_lock lock(s_androidLock); + if (s_intent.isValid()) { + return s_intent; + } + // QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]() { + if (!QNativeInterface::QAndroidApplication::isActivityContext()) { + __android_log_print(ANDROID_LOG_WARN, + "mixxx", + "current context doesn't refer to an activity!"); + } + + QJniObject context = QNativeInterface::QAndroidApplication::context(); + + s_usbManager = QJniObject("org/mixxx/UsbPermission"); + jint FLAG_IMMUTABLE = + QJniObject::getStaticField( + "android/app/PendingIntent", + "FLAG_IMMUTABLE"); + QtJniTypes::String ACTION_USB_PERMISSION = + QJniObject::fromString("org.mixxx.permissions.USB_PERMISSION"); + QtJniTypes::Intent intent = QJniObject("android/content/Intent", + "(Ljava/lang/String;)V", + ACTION_USB_PERMISSION.object()); + if (!intent.isValid()) { + __android_log_print(ANDROID_LOG_WARN, "mixxx", "pending intent is invalid!"); + } + s_intent = + QJniObject::callStaticMethod("android/app/PendingIntent", + "getBroadcast", + "(Landroid/content/Context;ILandroid/content/" + "Intent;I)Landroid/app/PendingIntent;", + context, + 0, + intent, + FLAG_IMMUTABLE); + + if (!s_intent.isValid()) { + __android_log_print(ANDROID_LOG_WARN, "mixxx", "pending intent is invalid!"); + } + + __android_log_print(ANDROID_LOG_VERBOSE, + "mixxx", + "about to register the the receiver %d", + s_usbManager.isValid()); + auto success = s_usbManager.callMethod("registerServiceBroadcastReceiver", + "(Landroid/content/Context;)Z", + context.object()); + if (!success) { + __android_log_print(ANDROID_LOG_WARN, "mixxx", "failed to registered the receiver!"); + } + // }); + return s_intent; +} + +bool waitForPermission(const QJniObject& device) { + __android_log_print(ANDROID_LOG_VERBOSE, "mixxx", "about to wait for perm"); + std::unique_lock lock(s_androidLock); + std::vector>::const_iterator result = s_grantingResult.cend(); + + int retries = 0; + + while (!s_grantingWaitCond.wait_for( + lock, std::chrono::seconds(1), [&result, device] { + result = std::find_if(s_grantingResult.cbegin(), + s_grantingResult.cend(), + [device](auto resultPair) { + return resultPair.first == device; + }); + return result != s_grantingResult.cend(); + })) { + __android_log_print(ANDROID_LOG_VERBOSE, + "mixxx", + "Not found - current result count: %lu", + s_grantingResult.size()); + QCoreApplication::processEvents(); + retries++; + if (retries >= 10) { + __android_log_print(ANDROID_LOG_WARN, "mixxx", "wait for perm timeout"); + qWarning() << "Timeout reached when waiting for Android permission to USB device"; + return false; + } + } + __android_log_print(ANDROID_LOG_VERBOSE, "mixxx", "got perm result"); + return result->second; +} + +void usbDeviceAccessResult(QJniObject device, bool granted) { + std::unique_lock lock(s_androidLock); + __android_log_print(ANDROID_LOG_WARN, "mixxx", "received permission result: %d", granted); + s_grantingResult.push_back(std::make_pair<>(device, granted)); + s_grantingWaitCond.notify_one(); + // FIXME Handle large list? +} +} // namespace android +} // namespace mixxx + +Q_DECLARE_JNI_CLASS(UsbPermissionClass, "org/mixxx/UsbPermission") + +void usbDeviceAccessResult(JNIEnv*, jobject, jobject device, jboolean granted) { + mixxx::android::usbDeviceAccessResult(device, granted); +} +Q_DECLARE_JNI_NATIVE_METHOD(usbDeviceAccessResult) + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM*, void*) { + QJniEnvironment env; + env.registerNativeMethods({ + Q_JNI_NATIVE_METHOD(usbDeviceAccessResult), + }); + return JNI_VERSION_1_6; +} diff --git a/src/controllers/android.h b/src/controllers/android.h new file mode 100644 index 000000000000..7c34d60cace6 --- /dev/null +++ b/src/controllers/android.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +struct libusb_context; + +namespace mixxx { +namespace android { + +const QJniObject& getIntent(); +bool waitForPermission(const QJniObject& device); +void usbDeviceAccessResult(QJniObject device, bool granted); + +extern std::mutex s_androidLock; +extern std::condition_variable s_grantingWaitCond; +extern std::vector> s_grantingResult; +extern QJniObject s_intent; +extern QJniObject s_usbManager; + +} // namespace android +} // namespace mixxx diff --git a/src/controllers/bulk/bulkcontroller.cpp b/src/controllers/bulk/bulkcontroller.cpp index e03d275fcded..a6d28787ab34 100644 --- a/src/controllers/bulk/bulkcontroller.cpp +++ b/src/controllers/bulk/bulkcontroller.cpp @@ -2,7 +2,9 @@ #include -#include +#if defined(Q_OS_ANDROID) +#include "controllers/android.h" +#endif #include "controllers/bulk/bulksupported.h" #include "controllers/defs_controllers.h" @@ -55,6 +57,7 @@ void BulkReader::run() { qDebug() << "Stopped Reader"; } +#ifndef Q_OS_ANDROID static QString get_string(libusb_device_handle* handle, uint8_t id) { unsigned char buf[128] = { 0 }; @@ -74,7 +77,8 @@ BulkController::BulkController(libusb_context* context, m_context(context), m_phandle(handle), m_inEndpointAddr(0), - m_outEndpointAddr(0) { + m_outEndpointAddr(0), + m_pReader(nullptr) { m_vendorId = desc->idVendor; m_productId = desc->idProduct; @@ -84,8 +88,34 @@ BulkController::BulkController(libusb_context* context, setInputDevice(true); setOutputDevice(true); - m_pReader = nullptr; } +#else +BulkController::BulkController(const QJniObject& usbDevice) + : Controller(QString("%1 %2").arg( + usbDevice.callMethod("getProductName").toString(), + usbDevice.callMethod("getSerialNumber").toString())), + m_context(nullptr), + m_phandle(nullptr), + m_androidUsbDevice(usbDevice), + m_inEndpointAddr(0), + m_outEndpointAddr(0), + m_pReader(nullptr) { + m_vendorId = static_cast(usbDevice.callMethod("getVendorId")); + m_productId = static_cast(usbDevice.callMethod("getProductId")); + + m_manufacturer = usbDevice.callMethod("getManufacturerName").toString(); + m_product = usbDevice.callMethod("getProductName").toString(); + m_sUID = usbDevice.callMethod("getSerialNumber").toString(); + if (m_sUID.isEmpty()) { + // Android won't allow reading serial number if permission wasn't + // granted previously. Is this an issue? + m_sUID = "N/A"; + } + + setInputDevice(true); + setOutputDevice(true); +} +#endif BulkController::~BulkController() { if (isOpen()) { @@ -184,6 +214,68 @@ int BulkController::open(const QString& resourcePath) { m_outEndpointAddr = pDevice->endpoints.out_epaddr; m_interfaceNumber = pDevice->endpoints.interface_number; +#ifdef __ANDROID__ + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject USB_SERVICE = + QJniObject::getStaticObjectField( + "android/content/Context", + "USB_SERVICE", + "Ljava/lang/String;"); + auto usbManager = context.callObjectMethod("getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + USB_SERVICE.object()); + if (!usbManager.isValid()) { + qDebug() << "usbManager invalid"; + return -1; + } + + if (!usbManager.callMethod("hasPermission", + "(Landroid/hardware/usb/UsbDevice;)Z", + m_androidUsbDevice)) { + auto pendingIntent = mixxx::android::getIntent(); + usbManager.callMethod("requestPermission", + "(Landroid/hardware/usb/UsbDevice;Landroid/app/" + "PendingIntent;)V", + m_androidUsbDevice, + pendingIntent); + // Wait for permission + if (!mixxx::android::waitForPermission(m_androidUsbDevice)) { + qDebug() << "access to device wasn't granted"; + return -1; + } + m_sUID = m_androidUsbDevice.callMethod("getSerialNumber").toString(); + } + m_androidConnection = usbManager.callMethod("openDevice", + "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/" + "UsbDeviceConnection;", + m_androidUsbDevice); + + if (!m_androidConnection.isValid()) { + qDebug() << "Unable to open BULK device"; + return -1; + } + + auto fileDescriptor = static_cast( + m_androidConnection.callMethod("getFileDescriptor")); + + // Open device by file descriptor + qCInfo(m_logBase) << "Opening BULK device" << getName() + << "by file descriptor" + << fileDescriptor << "and interface" + << m_interfaceNumber; + + libusb_set_option(nullptr, LIBUSB_OPTION_NO_DEVICE_DISCOVERY); + libusb_init(&m_context); + int error = libusb_wrap_sys_device(nullptr, (intptr_t)fileDescriptor, &m_phandle); + if (error < 0) { + qCWarning(m_logBase) << "Cannot open interface for" << getName() + << ":" << libusb_error_name(error); + libusb_close(m_phandle); + return -1; + } else { + qCDebug(m_logBase) << "Opened interface for" << getName(); + } +#else // XXX: we should enumerate devices and match vendor, product, and serial if (m_phandle == nullptr) { m_phandle = libusb_open_device_with_vid_pid( @@ -197,6 +289,7 @@ int BulkController::open(const QString& resourcePath) { if (libusb_set_auto_detach_kernel_driver(m_phandle, true) == LIBUSB_ERROR_NOT_SUPPORTED) { qCDebug(m_logBase) << "unable to automatically detach kernel driver for" << getName(); } +#endif if (m_interfaceNumber.has_value()) { int error = libusb_claim_interface(m_phandle, *m_interfaceNumber); @@ -272,6 +365,12 @@ int BulkController::close() { } qCInfo(m_logBase) << " Closing device"; libusb_close(m_phandle); + +#ifdef Q_OS_ANDROID + if (m_androidConnection.isValid()) { + m_androidConnection.callMethod("close"); + } +#endif m_phandle = nullptr; setOpen(false); return 0; diff --git a/src/controllers/bulk/bulkcontroller.h b/src/controllers/bulk/bulkcontroller.h index d9910e88a8c4..cd6b0b1f309b 100644 --- a/src/controllers/bulk/bulkcontroller.h +++ b/src/controllers/bulk/bulkcontroller.h @@ -3,6 +3,9 @@ #include #include #include +#ifdef Q_OS_ANDROID +#include +#endif #include "controllers/controller.h" #include "controllers/hid/legacyhidcontrollermapping.h" @@ -34,10 +37,15 @@ class BulkReader : public QThread { class BulkController : public Controller { Q_OBJECT public: +#ifndef Q_OS_ANDROID BulkController( libusb_context* context, libusb_device_handle* handle, struct libusb_device_descriptor* desc); +#else + BulkController( + const QJniObject& usbDevice); +#endif ~BulkController() override; QString mappingExtension() override; @@ -100,6 +108,10 @@ class BulkController : public Controller { libusb_context* m_context; libusb_device_handle *m_phandle; +#ifdef Q_OS_ANDROID + QJniObject m_androidUsbDevice; + QJniObject m_androidConnection; +#endif // Local copies of things we need from desc diff --git a/src/controllers/bulk/bulkenumerator.cpp b/src/controllers/bulk/bulkenumerator.cpp index 089a337a3cf0..4552a9f79fc3 100644 --- a/src/controllers/bulk/bulkenumerator.cpp +++ b/src/controllers/bulk/bulkenumerator.cpp @@ -1,35 +1,102 @@ #include "controllers/bulk/bulkenumerator.h" #include +#include + +#ifdef __ANDROID__ +#include +#include +#include +#include + +#include +#include +#endif #include "controllers/bulk/bulkcontroller.h" #include "controllers/bulk/bulksupported.h" #include "moc_bulkenumerator.cpp" +#include "util/assert.h" +#ifndef __ANDROID__ BulkEnumerator::BulkEnumerator() : ControllerEnumerator(), m_context(nullptr) { - libusb_init(&m_context); + int r; + r = libusb_init(&m_context); + VERIFY_OR_DEBUG_ASSERT(r == 0) { + qCritical() << "libusb_init failed" << libusb_error_name(r); + m_context = nullptr; + } } +#else +BulkEnumerator::BulkEnumerator() = default; +#endif BulkEnumerator::~BulkEnumerator() { qDebug() << "Deleting USB Bulk devices..."; while (m_devices.size() > 0) { delete m_devices.takeLast(); } - libusb_exit(m_context); +#ifndef __ANDROID__ + if (m_context) { + libusb_exit(m_context); + } +#endif } -static bool is_interesting(const libusb_device_descriptor& desc) { +static bool is_interesting(const uint16_t idVendor, const uint16_t idProduct) { return std::any_of(std::cbegin(bulk_supported), std::cend(bulk_supported), [&](const auto& dev) { - return dev.key.vendor_id == desc.idVendor && dev.key.product_id == desc.idProduct; + return dev.key.vendor_id == idVendor && dev.key.product_id == idProduct; }); } QList BulkEnumerator::queryDevices() { qDebug() << "Scanning USB Bulk devices:"; +#ifdef __ANDROID__ + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject USB_SERVICE = + QJniObject::getStaticObjectField( + "android/content/Context", + "USB_SERVICE", + "Ljava/lang/String;"); + auto usbManager = context.callObjectMethod("getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + USB_SERVICE.object()); + if (!usbManager.isValid()) { + qDebug() << "usbManager invalid"; + return {}; + } + + QJniObject deviceListObject = + usbManager.callMethod("getDeviceList", "()Ljava/util/HashMap;"); + deviceListObject = deviceListObject.callMethod("values", "()Ljava/util/Collection;"); + QJniArray deviceList = QJniArray( + deviceListObject.callMethod("toArray")); + __android_log_print(ANDROID_LOG_VERBOSE, + "mixxx", + "found %d USB devices for BULK enumerator", + deviceList.size()); + + for (const auto& usbDevice : deviceList) { + const uint16_t idVendor = static_cast( + usbDevice->callMethod("getVendorId")); + ; + const uint16_t idProduct = static_cast( + usbDevice->callMethod("getProductId")); + ; + if (is_interesting(idVendor, idProduct)) { + BulkController* currentDevice = + new BulkController(usbDevice); + m_devices.push_back(currentDevice); + } + } +#else + VERIFY_OR_DEBUG_ASSERT(m_context) { + return {}; + } libusb_device **list; ssize_t cnt = libusb_get_device_list(m_context, &list); ssize_t i = 0; @@ -40,7 +107,7 @@ QList BulkEnumerator::queryDevices() { struct libusb_device_descriptor desc; libusb_get_device_descriptor(device, &desc); - if (is_interesting(desc)) { + if (is_interesting(desc.idVendor, desc.idProduct)) { struct libusb_device_handle* handle = nullptr; err = libusb_open(device, &handle); if (err) { @@ -54,5 +121,6 @@ QList BulkEnumerator::queryDevices() { } } libusb_free_device_list(list, 1); +#endif return m_devices; } diff --git a/src/controllers/bulk/bulkenumerator.h b/src/controllers/bulk/bulkenumerator.h index 882fe808717e..222417078c3b 100644 --- a/src/controllers/bulk/bulkenumerator.h +++ b/src/controllers/bulk/bulkenumerator.h @@ -16,5 +16,7 @@ class BulkEnumerator : public ControllerEnumerator { private: QList m_devices; +#ifndef __ANDROID__ libusb_context* m_context; +#endif }; diff --git a/src/controllers/dlgprefcontroller.cpp b/src/controllers/dlgprefcontroller.cpp index 9c6798974b1f..4d80280028f7 100644 --- a/src/controllers/dlgprefcontroller.cpp +++ b/src/controllers/dlgprefcontroller.cpp @@ -18,7 +18,7 @@ #endif #include "controllers/defs_controllers.h" #include "controllers/dlgcontrollerlearning.h" -#ifdef __HID__ +#if defined(__HID__) && !defined(Q_OS_ANDROID) #include "controllers/hid/hidcontroller.h" #endif #include "controllers/midi/legacymidicontrollermapping.h" @@ -91,7 +91,7 @@ DlgPrefController::DlgPrefController( m_outputMappingsTabIndex(-1), m_settingsTabIndex(-1), m_screensTabIndex(-1) -#ifdef __HID__ +#if defined(__HID__) && !defined(Q_OS_ANDROID) , m_hidReportTabsManager(nullptr) { qRegisterMetaType(); @@ -204,7 +204,7 @@ DlgPrefController::DlgPrefController( m_ui.labelUsbInterfaceNumberValue->setVisible(false); } -#ifdef __HID__ +#if defined(__HID__) && !defined(Q_OS_ANDROID) // Display HID UsagePage and Usage if the controller is an HidController if (auto* hidController = qobject_cast(m_pController)) { m_ui.labelHidUsagePageValue->setText(QStringLiteral("%1 (%2)") diff --git a/src/controllers/dlgprefcontroller.h b/src/controllers/dlgprefcontroller.h index d5e6404299a2..879ae974d918 100644 --- a/src/controllers/dlgprefcontroller.h +++ b/src/controllers/dlgprefcontroller.h @@ -1,8 +1,10 @@ #pragma once +#include + #include -#ifdef __HID__ +#if defined(__HID__) && !defined(Q_OS_ANDROID) #include "controllers/controllerhidreporttabsmanager.h" #endif #include "controllers/controllermappinginfo.h" @@ -152,7 +154,7 @@ class DlgPrefController : public DlgPreferencePage { int m_screensTabIndex; // Index of the screens tab QHash m_settingsCollapsedStates; -#ifdef __HID__ +#if defined(__HID__) && !defined(Q_OS_ANDROID) std::unique_ptr m_hidReportTabsManager; #endif }; diff --git a/src/controllers/hid/hidcontroller.cpp b/src/controllers/hid/hidcontroller.cpp index 4807a9807297..7cff9c64c9dc 100644 --- a/src/controllers/hid/hidcontroller.cpp +++ b/src/controllers/hid/hidcontroller.cpp @@ -1,6 +1,13 @@ #include "controllers/hid/hidcontroller.h" +#ifdef __ANDROID__ +#include +#include + +#include "controllers/android.h" +#else #include +#endif #include "controllers/defs_controllers.h" #include "moc_hidcontroller.cpp" @@ -8,14 +15,17 @@ class LegacyControllerMapping; +#ifndef __ANDROID__ namespace { constexpr size_t kMaxHidErrorMessageSize = 512; } // namespace +#endif HidController::HidController( mixxx::hid::DeviceInfo&& deviceInfo) : Controller(deviceInfo.formatName()), - m_deviceInfo(std::move(deviceInfo)) { + m_deviceInfo(std::move(deviceInfo)), + m_deviceUsesReportIds(std::nullopt) { // We assume, that all HID controllers are full-duplex, // but a HID can also be input only (e.g. a USB HID mouse) setInputDevice(true); @@ -88,6 +98,65 @@ int HidController::open(const QString& resourcePath) { return -1; } +#ifdef __ANDROID__ + QJniObject usbDeviceConnection; + + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject USB_SERVICE = + QJniObject::getStaticObjectField( + "android/content/Context", + "USB_SERVICE", + "Ljava/lang/String;"); + auto usbManager = context.callObjectMethod("getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + USB_SERVICE.object()); + if (!usbManager.isValid()) { + qDebug() << "usbManager invalid"; + return -1; + } + + auto usbDevice = m_deviceInfo.androidUsbDevice(); + + if (!usbManager.callMethod("hasPermission", + "(Landroid/hardware/usb/UsbDevice;)Z", + usbDevice)) { + auto pendingIntent = mixxx::android::getIntent(); + usbManager.callMethod("requestPermission", + "(Landroid/hardware/usb/UsbDevice;Landroid/app/" + "PendingIntent;)V", + usbDevice, + pendingIntent); + // Wait for permission + if (!mixxx::android::waitForPermission(usbDevice)) { + qDebug() << "access to device wasn't granted"; + return -1; + } + m_deviceInfo.updateSerialNumber( + usbDevice.callMethod("getSerialNumber").toString()); + } + usbDeviceConnection = usbManager.callMethod("openDevice", + "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/" + "UsbDeviceConnection;", + usbDevice); + + if (!usbDeviceConnection.isValid()) { + qDebug() << "Unable to open HID device"; + return -1; + } + + auto fileDescriptor = static_cast( + usbDeviceConnection.callMethod("getFileDescriptor")); + + // Open device by file descriptor + qCInfo(m_logBase) << "Opening HID device" << getName() + << "by file descriptor" + << fileDescriptor << "and interface" + << m_deviceInfo.getUsbInterfaceNumber(); + + libusb_set_option(nullptr, LIBUSB_OPTION_NO_DEVICE_DISCOVERY); + hid_device* pHidDevice = hid_libusb_wrap_sys_device( + fileDescriptor, m_deviceInfo.getUsbInterfaceNumber().value()); +#else // Open device by path qCInfo(m_logBase) << "Opening HID device" << getName() << "by HID path" << m_deviceInfo.pathRaw(); @@ -154,6 +223,7 @@ int HidController::open(const QString& resourcePath) { kMaxHidErrorMessageSize)); return -1; } +#endif // Set hid controller to non-blocking if (hid_set_nonblocking(pHidDevice, 1) != 0) { @@ -162,6 +232,7 @@ int HidController::open(const QString& resourcePath) { return -1; } +#ifndef Q_OS_ANDROID // When fetching the report descriptor, from m_deviceInfo or if not read yet from the device const std::vector& rawReportDescriptor = m_deviceInfo.fetchRawReportDescriptor(pHidDevice); @@ -177,7 +248,11 @@ int HidController::open(const QString& resourcePath) { m_deviceUsesReportIds = std::nullopt; } +#endif m_pHidIoThread = std::make_unique(pHidDevice, m_deviceInfo, m_deviceUsesReportIds); +#ifdef Q_OS_ANDROID + m_pHidIoThread->setDeviceConnection(std::move(usbDeviceConnection)); +#endif m_pHidIoThread->setObjectName(QStringLiteral("HidIoThread ") + getName()); connect(m_pHidIoThread.get(), diff --git a/src/controllers/hid/hiddevice.cpp b/src/controllers/hid/hiddevice.cpp index 276345c24295..8c88d6a8c2c4 100644 --- a/src/controllers/hid/hiddevice.cpp +++ b/src/controllers/hid/hiddevice.cpp @@ -1,10 +1,13 @@ #include "controllers/hid/hiddevice.h" +#include + #include #include "controllers/controllermappinginfo.h" #include "util/string.h" +#ifndef Q_OS_ANDROID namespace { constexpr std::size_t kDeviceInfoStringMaxLength = 512; @@ -25,11 +28,13 @@ PhysicalTransportProtocol hidapiBusType2PhysicalTransportProtocol(hid_bus_type b } } // namespace +#endif namespace mixxx { namespace hid { +#ifndef Q_OS_ANDROID DeviceInfo::DeviceInfo(const hid_device_info& device_info) : vendor_id(device_info.vendor_id), product_id(device_info.product_id), @@ -51,6 +56,26 @@ DeviceInfo::DeviceInfo(const hid_device_info& device_info) m_serialNumber(mixxx::convertWCStringToQString( m_serialNumberRaw.data(), m_serialNumberRaw.size())) { } +#else +DeviceInfo::DeviceInfo( + const QJniObject& usbDevice, const QJniObject& usbInterface) + : m_androidUsbDevice(usbDevice), + m_physicalTransportProtocol(PhysicalTransportProtocol::USB) { + vendor_id = static_cast(usbDevice.callMethod("getVendorId")); + product_id = static_cast(usbDevice.callMethod("getProductId")); + m_manufacturerString = usbDevice.callMethod("getManufacturerName").toString(); + m_productString = usbDevice.callMethod("getProductName").toString(); + m_serialNumber = usbDevice.callMethod("getSerialNumber").toString(); + + if (m_serialNumber.isEmpty()) { + // Android won't allow reading serial number if permission wasn't + // granted previously. Is this an issue? + m_serialNumber = "N/A"; + } + + m_usbInterfaceNumber = usbInterface.callMethod("getId"); +} +#endif const std::vector& DeviceInfo::fetchRawReportDescriptor(hid_device* pHidDevice) { if (!pHidDevice) { @@ -144,11 +169,16 @@ bool DeviceInfo::matchProductInfo( } // Optionally check against m_usbInterfaceNumber / usage_page && usage - if (m_usbInterfaceNumber >= 0) { +#ifndef Q_OS_ANDROID + if (m_usbInterfaceNumber >= 0) +#endif + { if (m_usbInterfaceNumber != product.interface_number.toInt(&ok, 16) || !ok) { return false; } - } else { + } +#ifndef Q_OS_ANDROID + else { if (usage_page != product.usage_page.toInt(&ok, 16) || !ok) { return false; } @@ -156,6 +186,7 @@ bool DeviceInfo::matchProductInfo( return false; } } +#endif // Match found return true; } diff --git a/src/controllers/hid/hiddevice.h b/src/controllers/hid/hiddevice.h index 194be47969bd..0baf869f9897 100644 --- a/src/controllers/hid/hiddevice.h +++ b/src/controllers/hid/hiddevice.h @@ -1,9 +1,10 @@ #pragma once -#include - #include #include +#if defined(Q_OS_ANDROID) +#include +#endif #include #include #include @@ -13,6 +14,8 @@ struct ProductInfo; struct hid_device_info; +struct hid_device_; +typedef struct hid_device_ hid_device; namespace mixxx { @@ -32,8 +35,13 @@ namespace hid { /// QString if needed. class DeviceInfo final { public: +#ifndef Q_OS_ANDROID explicit DeviceInfo( const hid_device_info& device_info); +#else + explicit DeviceInfo( + const QJniObject& usbDevice, const QJniObject& usbInterface); +#endif // The VID. uint16_t getVendorId() const { @@ -44,6 +52,7 @@ class DeviceInfo final { return product_id; } +#ifndef Q_OS_ANDROID /// The releaseNumberBCD returns the version of the USB specification to /// which the device conforms. The bcdUSB field contains a BCD version /// number in the format 0xJJMN: @@ -67,6 +76,14 @@ class DeviceInfo final { const wchar_t* serialNumberRaw() const { return m_serialNumberRaw.c_str(); } +#else + const QJniObject& androidUsbDevice() const { + return m_androidUsbDevice; + } + void updateSerialNumber(QString serialNumber) { + m_serialNumber = serialNumber; + } +#endif const QString& getVendorString() const { return m_manufacturerString; @@ -90,19 +107,35 @@ class DeviceInfo final { } uint16_t getUsagePage() const { +#ifndef Q_OS_ANDROID return usage_page; +#else + return 0; +#endif } uint16_t getUsage() const { +#ifndef Q_OS_ANDROID return usage; +#else + return 0; +#endif } QString getUsagePageDescription() const { +#ifdef Q_OS_ANDROID + return QStringLiteral("N/A"); +#else return mixxx::hid::HidUsageTables::getUsagePageDescription(usage_page); +#endif } QString getUsageDescription() const { +#ifdef Q_OS_ANDROID + return QStringLiteral("N/A"); +#else return mixxx::hid::HidUsageTables::getUsageDescription(usage_page, usage); +#endif } // We need an opened hid_device here, @@ -111,7 +144,13 @@ class DeviceInfo final { const std::vector& fetchRawReportDescriptor(hid_device* pHidDevice); bool isValid() const { - return !getProductString().isNull() && !getSerialNumber().isNull(); + return !getProductString().isNull() +#ifdef Q_OS_ANDROID + && m_androidUsbDevice.isValid(); +#else + && !getSerialNumber().isNull(); +#endif + ; } QString formatName() const; @@ -127,15 +166,21 @@ class DeviceInfo final { // members from hid_device_info unsigned short vendor_id; unsigned short product_id; +#ifndef Q_OS_ANDROID unsigned short release_number; unsigned short usage_page; unsigned short usage; +#else + QJniObject m_androidUsbDevice; +#endif PhysicalTransportProtocol m_physicalTransportProtocol; int m_usbInterfaceNumber; +#ifndef Q_OS_ANDROID std::string m_pathRaw; std::wstring m_serialNumberRaw; +#endif QString m_manufacturerString; QString m_productString; diff --git a/src/controllers/hid/hidenumerator.cpp b/src/controllers/hid/hidenumerator.cpp index 20fbc3e2cce6..fcfbd2d9a647 100644 --- a/src/controllers/hid/hidenumerator.cpp +++ b/src/controllers/hid/hidenumerator.cpp @@ -1,6 +1,16 @@ #include "controllers/hid/hidenumerator.h" +#if defined(Q_OS_ANDROID) +#include +#include +#include +#include +#include + +#include +#else #include +#endif #include "controllers/hid/hidcontroller.h" #include "controllers/hid/hiddenylist.h" @@ -12,10 +22,12 @@ namespace mixxx { namespace hid { +#ifndef Q_OS_ANDROID constexpr unsigned short kGenericDesktopUsagePage = 0x01; constexpr unsigned short kGenericDesktopMouseUsage = 0x02; constexpr unsigned short kGenericDesktopKeyboardUsage = 0x06; +#endif // Apple has two two different vendor IDs which are used for different devices. constexpr unsigned short kAppleVendorId = 0x5ac; @@ -26,17 +38,24 @@ constexpr unsigned short kAppleIncVendorId = 0x004c; } // namespace mixxx namespace { - -bool recognizeDevice(const hid_device_info& device_info) { +bool recognizeDevice(unsigned short vendor_id, + unsigned short product_id, + int interface_number, + unsigned short usage_page = 0, + unsigned short usage = 0) { +// On Android, usage_page and usage are only accessible when permission is +// granted to the device, so we don't use it for device detection. +#ifndef Q_OS_ANDROID // Skip mice and keyboards. Users can accidentally disable their mouse // and/or keyboard by enabling them as HID controllers in Mixxx. // https://github.com/mixxxdj/mixxx/issues/10498 if (!CmdlineArgs::Instance().getDeveloper() && - device_info.usage_page == mixxx::hid::kGenericDesktopUsagePage && - (device_info.usage == mixxx::hid::kGenericDesktopMouseUsage || - device_info.usage == mixxx::hid::kGenericDesktopKeyboardUsage)) { + usage_page == mixxx::hid::kGenericDesktopUsagePage && + (usage == mixxx::hid::kGenericDesktopMouseUsage || + usage == mixxx::hid::kGenericDesktopKeyboardUsage)) { return false; } +#endif // Apple includes a variety of HID devices in their computers, not all of which // match the filter above for keyboards and mice, for example "Magic Trackpad", @@ -44,8 +63,7 @@ bool recognizeDevice(const hid_device_info& device_info) { // these devices in future computers and none of these devices are DJ controllers, // so skip all Apple HID devices rather than maintaining a list of specific devices // to skip. - if (device_info.vendor_id == mixxx::hid::kAppleVendorId - || device_info.vendor_id == mixxx::hid::kAppleIncVendorId) { + if (vendor_id == mixxx::hid::kAppleVendorId || vendor_id == mixxx::hid::kAppleIncVendorId) { return false; } @@ -53,29 +71,32 @@ bool recognizeDevice(const hid_device_info& device_info) { for (const hid_denylist_t& denylisted : hid_denylisted) { // If vendor ids are specified and do not match, skip. if (denylisted.vendor_id != kAnyValue && - device_info.vendor_id != denylisted.vendor_id) { + vendor_id != denylisted.vendor_id) { continue; } // If product IDs are specified and do not match, skip. if (denylisted.product_id != kAnyValue && - device_info.product_id != denylisted.product_id) { + product_id != denylisted.product_id) { continue; } // Denylist entry based on interface number // If interface number is present and the interface numbers do not // match, skip. if (denylisted.interface_number != kInvalidInterfaceNumber && - device_info.interface_number != denylisted.interface_number) { + interface_number != denylisted.interface_number) { continue; } +#ifdef Q_OS_ANDROID + continue; +#endif // Denylist entry based on usage_page and usage (both required) if (denylisted.usage_page != kAnyValue && denylisted.usage != kAnyValue) { // If usage_page is different, skip. - if (device_info.usage_page != denylisted.usage_page) { + if (usage_page != denylisted.usage_page) { continue; } // If usage is different, skip. - if (device_info.usage != denylisted.usage) { + if (usage != denylisted.usage) { continue; } } @@ -83,9 +104,16 @@ bool recognizeDevice(const hid_device_info& device_info) { } return true; } - } // namespace +bool HidEnumerator::recognizeDevice(const hid_device_info& device_info) const { + return ::recognizeDevice(device_info.vendor_id, + device_info.product_id, + device_info.interface_number, + device_info.usage_page, + device_info.usage); +} + HidEnumerator::~HidEnumerator() { qDebug() << "Deleting HID devices..."; while (m_devices.size() > 0) { @@ -97,6 +125,66 @@ HidEnumerator::~HidEnumerator() { QList HidEnumerator::queryDevices() { qInfo() << "Scanning USB HID devices"; +#ifdef __ANDROID__ + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject USB_SERVICE = + QJniObject::getStaticObjectField( + "android/content/Context", + "USB_SERVICE", + "Ljava/lang/String;"); + auto usbManager = context.callObjectMethod("getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + USB_SERVICE.object()); + if (!usbManager.isValid()) { + qDebug() << "usbManager invalid"; + return {}; + } + + QJniObject deviceListObject = + usbManager.callMethod("getDeviceList", "()Ljava/util/HashMap;"); + deviceListObject = deviceListObject.callMethod("values", "()Ljava/util/Collection;"); + QJniArray deviceList = QJniArray( + deviceListObject.callMethod("toArray")); + __android_log_print(ANDROID_LOG_VERBOSE, + "mixxx", + "found %d USB devices for HID enumerator", + deviceList.size()); + + for (const auto& usbDevice : deviceList) { + for (jint ifaceIdx = 0; + ifaceIdx < usbDevice->callMethod("getInterfaceCount"); + ifaceIdx++) { + auto usbInterface = usbDevice->callMethod("getInterface", + "(I)Landroid/hardware/usb/UsbInterface;", + ifaceIdx); + if (usbInterface.callMethod("getInterfaceClass") == LIBUSB_CLASS_HID) { + auto device_info = mixxx::hid::DeviceInfo(usbDevice, usbInterface); + + if (!::recognizeDevice(device_info.getVendorId(), + device_info.getProductId(), + device_info.getUsbInterfaceNumber().value())) { + qInfo() + << "Excluding HID device" + << device_info; + continue; + } + + qInfo() << "Found HID device:" + << device_info; + + if (!device_info.isValid()) { + qWarning() << "HID device permissions problem or device error." + << "Your account needs write access to HID controllers."; + continue; + } + + HidController* newDevice = new HidController(std::move(device_info)); + m_devices.push_back(newDevice); + } + } + } +#else + QStringList enumeratedDevices; hid_device_info* device_info_list = hid_enumerate(0x0, 0x0); for (const auto* device_info = device_info_list; @@ -131,6 +219,7 @@ QList HidEnumerator::queryDevices() { m_devices.push_back(newDevice); } hid_free_enumeration(device_info_list); +#endif return m_devices; } diff --git a/src/controllers/hid/hidenumerator.h b/src/controllers/hid/hidenumerator.h index af57f6414caa..b20542ad1798 100644 --- a/src/controllers/hid/hidenumerator.h +++ b/src/controllers/hid/hidenumerator.h @@ -8,6 +8,7 @@ class HidEnumerator : public ControllerEnumerator { Q_OBJECT public: + bool recognizeDevice(const hid_device_info& device_info) const; HidEnumerator() = default; ~HidEnumerator() override; diff --git a/src/controllers/hid/hidioglobaloutputreportfifo.h b/src/controllers/hid/hidioglobaloutputreportfifo.h index beffcc34c78d..399bcfbd592c 100644 --- a/src/controllers/hid/hidioglobaloutputreportfifo.h +++ b/src/controllers/hid/hidioglobaloutputreportfifo.h @@ -6,7 +6,7 @@ struct RuntimeLoggingCategory; class QMutex; - +struct hid_device_; typedef struct hid_device_ hid_device; namespace mixxx { diff --git a/src/controllers/hid/hidiothread.cpp b/src/controllers/hid/hidiothread.cpp index f4d05000c745..dd7fda383459 100644 --- a/src/controllers/hid/hidiothread.cpp +++ b/src/controllers/hid/hidiothread.cpp @@ -1,7 +1,13 @@ #include "controllers/hid/hidiothread.h" -#include +#include "util/assert.h" +#ifdef __ANDROID__ +#include +#include +#else +#include +#endif #include "moc_hidiothread.cpp" #include "util/runtimeloggingcategory.h" #include "util/string.h" @@ -56,6 +62,11 @@ HidIoThread::HidIoThread(hid_device* pHidDevice, HidIoThread::~HidIoThread() { hid_close(m_pHidDevice); +#ifdef Q_OS_ANDROID + if (m_androidConnection.isValid()) { + m_androidConnection.callMethod("close"); + } +#endif } void HidIoThread::run() { diff --git a/src/controllers/hid/hidiothread.h b/src/controllers/hid/hidiothread.h index 4520a9d26899..a7d5ff95fc1a 100644 --- a/src/controllers/hid/hidiothread.h +++ b/src/controllers/hid/hidiothread.h @@ -50,6 +50,15 @@ class HidIoThread : public QThread { void sendFeatureReport(quint8 reportID, const QByteArray& reportData); QByteArray getFeatureReport(quint8 reportID); +#ifdef Q_OS_ANDROID + // On Android, we open a connection to the device in JNI. we must keep the + // object alive and referenced to prevent GC and file descriptor being + // closed + void setDeviceConnection(QJniObject&& connection) { + m_androidConnection = connection; + } +#endif + signals: /// Signals that a HID InputReport received by Interrupt triggered from HID device void receive(const QByteArray& data, mixxx::Duration timestamp); @@ -104,4 +113,7 @@ class HidIoThread : public QThread { /// Semaphore with capacity 1, which is left acquired, as long as the run loop of the thread runs QSemaphore m_runLoopSemaphore; +#ifdef Q_OS_ANDROID + QJniObject m_androidConnection; +#endif }; diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index 93f1a1d92879..78b728a88e0c 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -54,6 +54,9 @@ ControllerScriptEngineLegacy::~ControllerScriptEngineLegacy() { } void ControllerScriptEngineLegacy::watchFilePath(const QString& path) { +#ifdef __ANDROID__ + return; +#endif if (m_fileWatcher.files().contains(path) || m_fileWatcher.directories().contains(path)) { qCDebug(m_logger) << "File" << path << "is already being watch for controller auto-reload"; return; diff --git a/src/coreservices.cpp b/src/coreservices.cpp index a96a9e286404..f81918969398 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -67,6 +67,9 @@ #if defined(Q_OS_LINUX) && defined(__X11__) #include #endif +#if defined(Q_OS_ANDROID) +#include +#endif #if defined(Q_OS_LINUX) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include @@ -626,6 +629,44 @@ void CoreServices::initialize(QApplication* pApp) { if (!dir.exists()) { dir.mkpath("."); } +#elif defined(Q_OS_ANDROID) + // if(QOperatingSystemVersion::current() < + // QOperatingSystemVersion(QOperatingSystemVersion::Android, 11)) { + // qDebug() << "it is less then Android 11 - ALL FILES permission + // isn't possible!"; + // } + QString fd; + jboolean value = QJniObject::callStaticMethod( + "android/os/Environment", "isExternalStorageManager"); + if (value == false) { + qDebug() << "requesting ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION"; + QJniObject ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION = + QJniObject::getStaticObjectField( + "android/provider/Settings", + "ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION", + "Ljava/lang/String;"); + QJniObject intent("android/content/Intent", + "(Ljava/lang/String;)V", + ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION.object()); + QJniObject jniPath = QJniObject::fromString( + QStringLiteral("package:%1").arg(ANDROID_PACKAGE_NAME)); + QJniObject jniUri = + QJniObject::callStaticObjectMethod("android/net/Uri", + "parse", + "(Ljava/lang/String;)Landroid/net/Uri;", + jniPath.object()); + QJniObject jniResult = intent.callObjectMethod("setData", + "(Landroid/net/Uri;)Landroid/content/Intent;", + jniUri.object()); + QtAndroidPrivate::startActivity(intent, 0); + } else { + qDebug() << "Got ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION"; + } + fd = "/storage/emulated/0/Music/"; + QDir dir = fd; + if (!dir.exists()) { + dir.mkpath("."); + } #else // TODO(XXX) this needs to be smarter, we can't distinguish between an empty // path return value (not sure if this is normally possible, but it is diff --git a/src/mixxxmainwindow.cpp b/src/mixxxmainwindow.cpp index bc902e1fc54b..84fb4c245d6a 100644 --- a/src/mixxxmainwindow.cpp +++ b/src/mixxxmainwindow.cpp @@ -11,7 +11,7 @@ #include #endif -#ifdef __LINUX__ +#if defined(__LINUX__) && !defined(__ANDROID__) #include #include #endif diff --git a/src/preferences/configobject.cpp b/src/preferences/configobject.cpp index 81709fada49b..e3758e3726e6 100644 --- a/src/preferences/configobject.cpp +++ b/src/preferences/configobject.cpp @@ -65,7 +65,7 @@ QString computeResourcePathImpl() { "'--resource-path '."); } } -#if defined(__UNIX__) +#if defined(__UNIX__) && !defined(__ANDROID__) else if (mixxxDir.cd(QStringLiteral("../share/mixxx"))) { qResourcePath = mixxxDir.absolutePath(); } @@ -75,6 +75,11 @@ QString computeResourcePathImpl() { else { qResourcePath = QCoreApplication::applicationDirPath(); } +#elif defined(__ANDROID__) + // On Android, use the QRC. + else { + qResourcePath = "assets:/"; + } #elif defined(Q_OS_IOS) // On iOS the bundle contains the resources directly. else { diff --git a/src/qml/qmlapplication.cpp b/src/qml/qmlapplication.cpp index 973c74ac4724..922048869925 100644 --- a/src/qml/qmlapplication.cpp +++ b/src/qml/qmlapplication.cpp @@ -95,16 +95,6 @@ QmlApplication::QmlApplication( // follows a strict singleton pattern design QmlDlgPreferencesProxy::s_pInstance = std::make_unique(pDlgPreferences, this); - loadQml(m_mainFilePath); - - m_pCoreServices->getControllerManager()->setUpDevices(); - - connect(&m_autoReload, - &QmlAutoReload::triggered, - this, - [this]() { - loadQml(m_mainFilePath); - }); const QStringList visualGroups = m_pCoreServices->getPlayerManager()->getVisualPlayerGroups(); @@ -122,6 +112,16 @@ QmlApplication::QmlApplication( m_visualsManager->addDeckIfNotExist(group); } }); + loadQml(m_mainFilePath); + + m_pCoreServices->getControllerManager()->setUpDevices(); + + connect(&m_autoReload, + &QmlAutoReload::triggered, + this, + [this]() { + loadQml(m_mainFilePath); + }); } QmlApplication::~QmlApplication() { diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index 345d0e8aec5a..770eda992ac8 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "control/controlobject.h" #include "sounddevicenetwork.h" @@ -27,6 +28,11 @@ #include #endif +#ifdef PA_USE_OBOE +// for PaOboe_InitializeStreamInfo +#include +#endif + namespace { // Buffer for drift correction 1 full, 1 for r/w, 1 empty @@ -241,7 +247,24 @@ SoundDeviceStatus SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffer m_outputParams.device = m_deviceId.portAudioIndex; m_outputParams.sampleFormat = paFloat32; m_outputParams.suggestedLatency = bufferMSec / 1000.0; - m_outputParams.hostApiSpecificStreamInfo = nullptr; +#ifdef PA_USE_OBOE + PaOboeStreamInfo obeoStreamInfo; + if (m_deviceTypeId == PaHostApiTypeId::paOboe) { + PaOboe_InitializeStreamInfo(&obeoStreamInfo); + obeoStreamInfo.androidOutputUsage = PaOboe_Usage::Media, + obeoStreamInfo.androidInputPreset = PaOboe_InputPreset::Generic, + obeoStreamInfo.performanceMode = PaOboe_PerformanceMode::LowLatency, + obeoStreamInfo.sharingMode = PaOboe_SharingMode::Exclusive, + obeoStreamInfo.contentType = PaOboe_ContentType::Music, + obeoStreamInfo.packageName = ANDROID_PACKAGE_NAME; + + m_outputParams.hostApiSpecificStreamInfo = (void*)&obeoStreamInfo; + } else { +#endif + m_outputParams.hostApiSpecificStreamInfo = nullptr; +#ifdef PA_USE_OBOE + } +#endif m_inputParams.device = m_deviceId.portAudioIndex; m_inputParams.sampleFormat = paFloat32; diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index 4241696aa37d..4dd83adc7d66 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -25,6 +25,12 @@ #ifdef Q_OS_IOS #include "soundio/soundmanagerios.h" +#elif defined(Q_OS_ANDROID) +#include +#include +#include + +#include #endif typedef PaError (*SetJackClientName)(const char *name); @@ -121,6 +127,9 @@ QList SoundManager::getDeviceList( // make sure to include any devices that have >0 channels. const bool hasOutputs = pDevice->getNumOutputChannels().isValid(); const bool hasInputs = pDevice->getNumInputChannels().isValid(); + qDebug() << "SoundManager::getDeviceList" << pDevice->getHostAPI() + << filterAPI << pDevice->getNumOutputChannels() + << pDevice->getNumInputChannels(); if (pDevice->getHostAPI() != filterAPI || (bOutputDevices && !bInputDevices && !hasOutputs) || (bInputDevices && !bOutputDevices && !hasInputs) || @@ -257,7 +266,7 @@ QList SoundManager::getSampleRates() const { } void SoundManager::queryDevices() { - //qDebug() << "SoundManager::queryDevices()"; + qDebug() << "SoundManager::queryDevices()"; queryDevicesPortaudio(); queryDevicesMixxx(); @@ -274,11 +283,122 @@ void SoundManager::clearAndQueryDevices() { void SoundManager::queryDevicesPortaudio() { PaError err = paNoError; if (!m_paInitialized) { -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) setJACKName(); #endif #ifdef Q_OS_IOS mixxx::initializeAVAudioSession(); +#elif defined(Q_OS_ANDROID) + QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]() { + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject AUDIO_SERVICE = + QJniObject::getStaticObjectField( + "android/content/Context", + "AUDIO_SERVICE", + "Ljava/lang/String;"); + auto audioManager = context.callObjectMethod("getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + AUDIO_SERVICE.object()); + if (!audioManager.isValid()) { + qDebug() << "audioManager invalid"; + return; + } + qDebug() << "audioManager valid:" << audioManager.toString(); + + jint GET_DEVICES_INPUTS = + QJniObject::getStaticField( + "android/media/AudioManager", + "GET_DEVICES_INPUTS"); + jint GET_DEVICES_OUTPUTS = + QJniObject::getStaticField( + "android/media/AudioManager", + "GET_DEVICES_OUTPUTS"); + + auto const isSupported = [](int type) { + switch (type) { + case 1: // AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + case 2: // AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + case 3: // AudioDeviceInfo.TYPE_WIRED_HEADSET + case 8: // AudioDeviceInfo.TYPE_BLUETOOTH_A2DP + case 11: // AudioDeviceInfo.TYPE_USB_DEVICE + case 22: // AudioDeviceInfo.TYPE_USB_HEADSET + case 9: // AudioDeviceInfo.TYPE_HDMI + case 10: // AudioDeviceInfo.TYPE_HDMI_ARC + case 13: // AudioDeviceInfo.TYPE_DOCK + case 15: // AudioDeviceInfo.TYPE_BUILTIN_MIC + case 12: // AudioDeviceInfo.TYPE_USB_ACCESSORY + case 26: // AudioDeviceInfo.TYPE_BLE_HEADSET + case 27: // AudioDeviceInfo.TYPE_BLE_SPEAKER + case 23: // AudioDeviceInfo.TYPE_HEARING_AID + case 25: // AudioDeviceInfo.TYPE_REMOTE_SUBMIX: + // supported + return true; + default: + // unsupported + break; + } + return false; + }; + + auto const parse = [isSupported](PaOboe_Direction direction, + QJniArray& devices) { + for (const auto& device : devices) { + jint type = device->callMethod("getType"); + if (!isSupported(type)) { + continue; + } + QString name = device->callObjectMethod("getProductName", + "()Ljava/lang/CharSequence;") + .toString(); + auto channelCounts = device->callMethod>("getChannelCounts"); + int channelCount = *std::max_element( + channelCounts.begin(), channelCounts.end()); + auto sampleRates = device->callMethod>("getSampleRates"); + qDebug() << "audioManager - Type:" << type + << "- Name:" << name + << "- ChannelCount:" << channelCount + << channelCounts.size(); + if (!sampleRates.isEmpty()) { + int sampleRate = *sampleRates.cbegin(); + qDebug() << "audioManager - SampleRates:" << sampleRate; + PaOboe_RegisterDevice(name.toStdString().c_str(), + direction, + channelCount, + sampleRate); + } + } + }; + + auto inputDevices = + audioManager.callMethod>("getDevices", + "(I)[Landroid/media/AudioDeviceInfo;", + GET_DEVICES_INPUTS); + qDebug() << "audioManager inputDevices:" << inputDevices.size(); + parse(PaOboe_Direction::Input, inputDevices); + + auto outputDevices = + audioManager.callMethod>("getDevices", + "(I)[Landroid/media/AudioDeviceInfo;", + GET_DEVICES_OUTPUTS); + qDebug() << "audioManager outputDevices:" << outputDevices.size(); + parse(PaOboe_Direction::Output, outputDevices); + + QJniObject PROPERTY_OUTPUT_FRAMES_PER_BUFFER = + QJniObject::getStaticField( + "android/media/AudioManager", + "PROPERTY_OUTPUT_FRAMES_PER_BUFFER"); + auto outputFramePerBuffer = + audioManager + .callMethod("getProperty", + "(Ljava/lang/String;)Ljava/lang/String;", + PROPERTY_OUTPUT_FRAMES_PER_BUFFER) + .toString() + .toUInt(); + qDebug() << "audioManager outputFramePerBuffer:" << outputFramePerBuffer; + PaOboe_SetNativeBufferSize(outputFramePerBuffer); + }).waitForFinished(); + PaOboe_SetNumberOfBuffers(1); + #endif err = Pa_Initialize(); m_paInitialized = true; @@ -291,9 +411,14 @@ void SoundManager::queryDevicesPortaudio() { int iNumDevices = Pa_GetDeviceCount(); if (iNumDevices < 0) { - qDebug() << "ERROR: Pa_CountDevices returned" << iNumDevices; + qDebug() << "ERROR: Pa_CountDevices returned" << Pa_GetErrorText(iNumDevices); return; + } else if (iNumDevices == 0) { + qWarning() << "Pa_CountDevices returned no devices!"; + } else { + qDebug() << "Pa_CountDevices found" << iNumDevices << "devices"; } + qDebug() << "Pa_GetHostApiCount returns" << Pa_GetHostApiCount(); // PaDeviceInfo structs have a PaHostApiIndex member, but PortAudio // unfortunately provides no good way to associate this with a persistent, @@ -310,6 +435,7 @@ void SoundManager::queryDevicesPortaudio() { const PaDeviceInfo* deviceInfo; for (int i = 0; i < iNumDevices; i++) { deviceInfo = Pa_GetDeviceInfo(i); + qDebug() << "Pa_GetDeviceInfo on" << i << deviceInfo; if (!deviceInfo) { continue; } diff --git a/src/util/cmdlineargs.cpp b/src/util/cmdlineargs.cpp index 1dfdc4cb2549..51931952ea47 100644 --- a/src/util/cmdlineargs.cpp +++ b/src/util/cmdlineargs.cpp @@ -69,10 +69,12 @@ CmdlineArgs::CmdlineArgs() m_logFlushLevel(mixxx::kLogFlushLevelDefault), m_logMaxFileSize(mixxx::kLogMaxFileSizeDefault), // We are not ready to switch to XDG folders under Linux, so keeping $HOME/.mixxx as preferences folder. see #8090 +#if defined(__LINUX__) #ifdef MIXXX_SETTINGS_PATH m_settingsPath(QDir::homePath().append("/").append(MIXXX_SETTINGS_PATH)) -#elif defined(__LINUX__) +#else #error "We are not ready to switch to XDG folders under Linux" +#endif #elif defined(Q_OS_IOS) // On iOS we intentionally use a user-accessible subdirectory of the sandbox // documents directory rather than the default app data directory. Specifically diff --git a/src/util/desktophelper.cpp b/src/util/desktophelper.cpp index 8cf849ba68ac..8124d27d7a0f 100644 --- a/src/util/desktophelper.cpp +++ b/src/util/desktophelper.cpp @@ -7,7 +7,7 @@ #include #include -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) #include #include #include @@ -24,6 +24,9 @@ QString getSelectInFileBrowserCommand() { return "open -R"; #elif defined(Q_OS_WIN) return "explorer.exe /select,"; +#elif defined(Q_OS_ANDROID) + // TODO emit android intent + return ""; #elif defined(Q_OS_LINUX) QProcess proc; QString output; @@ -58,7 +61,7 @@ QString removeChildDir(const QString& path) { return path.left(index); } -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) bool selectInFreedesktop(const QString& path) { const QUrl fileurl = QUrl::fromLocalFile(path); const QStringList args(fileurl.toString()); @@ -125,7 +128,7 @@ void DesktopHelper::openInFileBrowser(const QStringList& paths) { if (fileInfo.exists()) { // Tryto select the file -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) if (sSelectInFileBrowserCommand == kSelectInFreedesktop) { if (selectInFreedesktop(path)) { openedDirs.insert(dirPath); diff --git a/src/util/screensaver.cpp b/src/util/screensaver.cpp index c403c3c10400..896fb52dd714 100644 --- a/src/util/screensaver.cpp +++ b/src/util/screensaver.cpp @@ -26,6 +26,10 @@ With the help of the following source codes: # include "util/mac.h" #elif defined(Q_OS_IOS) #include "util/screensaverios.h" +#elif defined(Q_OS_ANDROID) +#include +#include +#define HAS_XWINDOW_SCREENSAVER 0 #elif defined(_WIN32) # include #elif defined(__LINUX__) @@ -347,6 +351,52 @@ void ScreenSaverHelper::inhibitInternal() { } void ScreenSaverHelper::uninhibitInternal() { } +#elif defined(Q_OS_ANDROID) + +QJniObject ScreenSaverHelper::s_wakeLock = {}; +// Screensavers are not supported +void ScreenSaverHelper::triggerUserActivity() { +} +void ScreenSaverHelper::inhibitInternal() { + if (!ScreenSaverHelper::s_wakeLock.isValid()) { + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject POWER_SERVICE = + QJniObject::getStaticObjectField( + "android/content/Context", + "POWER_SERVICE", + "Ljava/lang/String;"); + auto powerService = context.callObjectMethod("getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;", + POWER_SERVICE.object()); + if (!powerService.isValid()) { + qDebug() << "powerService invalid"; + return; + } + + jint FULL_WAKE_LOCK = + QJniObject::getStaticField( + "android/os/PowerManager", + "FULL_WAKE_LOCK"); + ScreenSaverHelper::s_wakeLock = + powerService.callObjectMethod("newWakeLock", + "(ILjava/lang/String;)Landroid/os/PowerManager$WakeLock;", + FULL_WAKE_LOCK, + QJniObject::fromString("Mixxx").object()); + if (!ScreenSaverHelper::s_wakeLock.isValid()) { + __android_log_print(ANDROID_LOG_WARN, "mixxx", "powerService wakeLock invalid"); + qWarning() << "ScreenSaverHelper::inhibitInternal - wakeLock invalid"; + return; + } + } + ScreenSaverHelper::s_wakeLock.callMethod("acquire"); +} +void ScreenSaverHelper::uninhibitInternal() { + // QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]() { + if (ScreenSaverHelper::s_wakeLock.isValid()) { + ScreenSaverHelper::s_wakeLock.callMethod("release"); + } + // }).waitForFinished(); +} #else void ScreenSaverHelper::triggerUserActivity() { diff --git a/src/util/screensaver.h b/src/util/screensaver.h index a1f5048d6aba..191a103caa44 100644 --- a/src/util/screensaver.h +++ b/src/util/screensaver.h @@ -29,6 +29,8 @@ class ScreenSaverHelper { /* sleep management */ static IOPMAssertionID s_systemSleepAssertionID; static IOPMAssertionID s_userActivityAssertionID; +#elif defined(Q_OS_ANDROID) + static QJniObject s_wakeLock; #elif defined(Q_OS_LINUX) static uint32_t s_cookie; static int s_saverindex; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index 0de8551a12cd..5f59e5045102 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -10,6 +10,9 @@ #include #include #endif +#ifdef Q_OS_ANDROID +#include +#endif #include #include diff --git a/src/widget/wspinnyglsl.cpp b/src/widget/wspinnyglsl.cpp index 6199f848e1de..3515e78a96c3 100644 --- a/src/widget/wspinnyglsl.cpp +++ b/src/widget/wspinnyglsl.cpp @@ -84,7 +84,11 @@ void WSpinnyGLSL::updateVinylSignalQualityImage( 0, m_iVinylScopeSize, m_iVinylScopeSize, +#if defined(GL_RED) GL_RED, +#else + GL_RED_EXT, +#endif GL_UNSIGNED_BYTE, data); m_qTexture.release(); diff --git a/tools/android_buildenv.sh b/tools/android_buildenv.sh new file mode 100755 index 000000000000..858be57576fb --- /dev/null +++ b/tools/android_buildenv.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Ignored in case of a source call, but needed for bash specific sourcing detection + +set -o pipefail + +# shellcheck disable=SC2091 +if [ -z "${GITHUB_ENV}" ] && ! $(return 0 2>/dev/null); then + echo "This script must be run by sourcing it:" + echo "source $0 $*" + exit 1 +fi + +realpath() { + OLDPWD="${PWD}" + cd "$1" || exit 1 + pwd + cd "${OLDPWD}" || exit 1 +} + +# Get script file location, compatible with bash and zsh +if [ -n "$BASH_VERSION" ]; then + THIS_SCRIPT_NAME="${BASH_SOURCE[0]}" +elif [ -n "$ZSH_VERSION" ]; then + # shellcheck disable=SC2296 + THIS_SCRIPT_NAME="${(%):-%N}" +else + THIS_SCRIPT_NAME="$0" +fi + +HOST_ARCH=$(uname -m) # One of x86_64, arm64, i386, ppc or ppc64 + +if [ "$HOST_ARCH" == "x86_64" ]; then + if [ -n "${BUILDENV_RELEASE}" ]; then + echo "ERROR: No release VCPKG for Android yet!" + exit 1 + else + VCPKG_TARGET_TRIPLET="arm64-android" + BUILDENV_BRANCH="2.7" + BUILDENV_NAME="mixxx-deps-2.7-arm64-android-9dcfaf7" + BUILDENV_SHA256="0821e7d4f6b989ed5acc3c9a8dafa00f479d9d8bfc8dea9f9b512816070c9bba" + fi +else + echo "ERROR: Unsupported architecture detected: $HOST_ARCH" + echo "Please refer to the following guide to manually build the vcpkg environment:" + echo "https://github.com/mixxxdj/mixxx/wiki/Compiling-dependencies-for-android" + exit 1 +fi + +BUILDENV_URL="https://downloads.mixxx.org/dependencies/${BUILDENV_BRANCH}/Linux/${BUILDENV_NAME}.zip" +MIXXX_ROOT="$(realpath "$(dirname "$THIS_SCRIPT_NAME")/..")" +ANDROID_API=35 +ANDROID_VERSION=35.0.0 +ANDROID_NDK=27.2.12479018 + +[ -z "$BUILDENV_BASEPATH" ] && BUILDENV_BASEPATH="${MIXXX_ROOT}/buildenv" + +case "$1" in + name) + if [ -n "${GITHUB_ENV}" ]; then + echo "BUILDENV_NAME=$BUILDENV_NAME" >> "${GITHUB_ENV}" + else + echo "$BUILDENV_NAME" + fi + ;; + + setup) + sudo apt-get install -y --no-install-recommends -- \ + ccache \ + cmake \ + make \ + build-essential \ + '^libxcb.*-dev' \ + autoconf \ + autoconf-archive \ + bison \ + flex \ + google-android-cmdline-tools-13.0-installer \ + libasound2-dev \ + libegl1-mesa-dev \ + libghc-resolv-dev \ + libglu1-mesa-dev \ + libltdl-dev \ + libx11-xcb-dev \ + libxi-dev \ + libxkbcommon-dev \ + libxkbcommon-x11-dev \ + libxrender-dev \ + linux-libc-dev \ + openjdk-17-jdk \ + pkg-config \ + python3-jinja2 + yes | sudo sdkmanager --licenses + sudo sdkmanager "platforms;android-${ANDROID_API}" "platform-tools" "build-tools;${ANDROID_VERSION}" "ndk;${ANDROID_NDK}" + ANDROID_SDK=/usr/lib/android-sdk + ANDROID_NDK_HOME=/usr/lib/android-sdk/ndk/${ANDROID_NDK} + JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-17-openjdk*') + export ANDROID_SDK + export ANDROID_NDK_HOME + export JAVA_HOME + BUILDENV_PATH="${BUILDENV_BASEPATH}/${BUILDENV_NAME}" + + export BUILDENV_NAME + export BUILDENV_BASEPATH + export BUILDENV_URL + export BUILDENV_SHA256 + export MIXXX_VCPKG_ROOT="${BUILDENV_PATH}" + export VCPKG_TARGET_TRIPLET="${VCPKG_TARGET_TRIPLET}" + + echo_exported_variables() { + echo "ANDROID_SDK=${ANDROID_SDK}" + echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}" + echo "JAVA_HOME=${JAVA_HOME}" + echo "BUILDENV_NAME=${BUILDENV_NAME}" + echo "BUILDENV_BASEPATH=${BUILDENV_BASEPATH}" + echo "BUILDENV_URL=${BUILDENV_URL}" + echo "BUILDENV_SHA256=${BUILDENV_SHA256}" + echo "MIXXX_VCPKG_ROOT=${MIXXX_VCPKG_ROOT}" + echo "VCPKG_TARGET_TRIPLET=${VCPKG_TARGET_TRIPLET}" + } + + if [ -n "${GITHUB_ENV}" ]; then + echo_exported_variables >> "${GITHUB_ENV}" + elif [ "$1" != "--profile" ]; then + echo "" + echo "Exported environment variables:" + echo_exported_variables + echo "You can now configure cmake from the command line in an EMPTY build directory via:" + echo "cmake -DCMAKE_TOOLCHAIN_FILE=${MIXXX_VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake -DCMAKE_SYSTEM_NAME=Android ${MIXXX_ROOT}" + fi + ;; + *) + echo "Usage: source android_buildenv.sh [options]" + echo "" + echo "options:" + echo " help Displays this help." + echo " name Displays the name of the required build environment." + echo " setup Setup the build environment variables for download during CMake configuration and install the build environment." + ;; +esac